简单的树遍历枚举器v0.2-挑战一个程序员到底能多懒- 添加广度优先遍历

前一阵在递归算法相关回贴的讨论中 和某lz抱怨 现在的同志们连用自己的栈加循环模拟递归都不会做了。
如果自己实现递归栈  又怎么会在线程栈中储存过多无关信息?数据全部都在堆里 又怎会stackoverflow?
当时就有想法自己实现一个,造福一下群众,但是被坏心眼的某lz 阻止了。“让他们自己写。” 他大概是这么说滴,“他们自己写了才算懂得了。”

可是 最近自己在工程中越来越多必须实现“数据库树型菜单表”“对游戏大厅内所有子孙房间进行检查” “对两个子目录中所有文件进行比较”这样的树型遍历操作
用递归 自己觉得恶心, 用栈每次重写麻烦。 
又想起用linq to xml 的 System.Xml.Linq.Extensions.Descendants<T>是多么畅快 为啥xpath可以 linq to xml可以  咱就不能用一个扩展方法把所有想遍历的树统统解决掉呢?

 于是花了一个下午写了一个枚举器, 用循环来代替栈,让溢出见鬼去吧!


-----------------------------------------------思路分割线--------------------------------------------------------------
问题:什么是树的遍历。
回答:树的遍历是树的一种重要的运算。所谓 遍历是指对树中所有结点的系统的访问,即依次对树中每个结点访问一次且仅访问一次。
 
问题:树的遍历一般过程?
回答:
朱德庸某绝对小孩2 中有一幅漫画:
心理辅导师对 顽皮:“你的问题很严重 我想找你的父母谈谈”
心理辅导师对 顽皮父母:“你们的问题很严重 我想找你们的父母谈谈”
心理辅导师对 顽皮祖父母:“你们的问题很严重 我想找你们的父母谈谈”
。。。。

顽皮-
  父亲
      父亲的父亲
            父亲的父亲的父亲
            父亲的父亲的母亲
      父亲的母亲
  母亲
      母亲的父亲
      母亲的母亲
。。。。
。。。
 不解决坟墓里面的曾祖父母们  是没办法解决祖父母们 和 父母的问题 也就没办法解决顽皮的问题。

1 访问本节点
2 有子节点则 
   对每一个子节点
            {
                  1 访问本节点 
                  2 有子节点则 
                     对每一个子节点
                        {
                              ....
                        }
            }



问题:为什么树的遍历要用到递归
回答: 递归调用“处理到一半 先搁置”的特性 非常符合树的遍历访问。

问题:为什么你做树的遍历不希望用递归
回答: 递归的时候 经常会把不需要的东西压到栈里( 为什么要尾递归)。这个栈毕竟是有限的,我们可以自己把处理一半的东放在自己在堆实现的栈 而不是线程分配的那可怜的1MB。我们可以专注在我们需要的数据上

问题:为什么要用枚举器
回答: 好处多多,  结果直接支持linq查询, 支持lazy方式提高枚举效率 想变list就list  想变dictionary 就dictionary 


问题:不同树的相同点是什么
回答:  所有的节点都实现相同的接口,基本上是同质的  而且他们都有子节点的集合
· 顽皮家所有成员都是人生父母养的
· 所有的目录都可能有子目录和文件
· 所有的XML节点都可以具有标记名 属性 和子节点

问题:不同树的不同点是什么
回答: 如何取得子节点集合
· 联系一个人的父母要查通讯录
· 访问子目录要使用  io.directory.getdirectories()
· 访问XML子节点要访问childrennodes集合
-----------------------------------------------思路分割线--------------------------------------------------------------



理清思路 我们需要的是一个以根节点和从根节点选择子节点集合的方法为参数创建的范型的枚举器
    public class RecursionEnumerator<TItem> : System.Collections.Generic.IEnumerator<TItem>, System.Collections.IEnumerator ,IEnumerator <Stack<TItem >>
ExpandedBlockStart.gifContractedBlock.gif    
{


        
public RecursionEnumerator(IEnumerable<TItem> rootObjects, Func<TItem, IEnumerable<TItem>>
 childrenSelector)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
{

            
        }

}

枚举器最重要的方法实现当然是MoveNext()了
整体工作流程和一般递归调用一致
 
   对每一个子节点
            {
                  (把当前子节点枚举器压入自定义Stack)
                  1 访问本节点 
                  2 有子节点则 
                     对每一个子节点
                        {
                              (把当前子节点枚举器压入自定义Stack)
                              ....
                              (把当前子节点枚举器从自定义Stack弹出)
                        }
                  (把当前子节点枚举器从自定义Stack弹出)
            }

这里提供了注释

     public bool  MoveNext()
ExpandedBlockStart.gifContractedBlock.gif        
{
            
if (ColStack.Count > 0
)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{

                var cur 
=
 ColStack.Peek();

                
if (Started)  //已经开始

ExpandedSubBlockStart.gifContractedSubBlock.gif
                {
                    
//主动找下一个节点

                    var en = ChildrenSelector(cur.Current).GetEnumerator(); //先看有没有子节点

                    
if (en.MoveNext()) //有子节点  
ExpandedSubBlockStart.gifContractedSubBlock.gif
                    {
                        
// 把这个层加入堆栈 访问第一个节点

                        ColStack.Push(en);
                        
return true
;
                    }

                    
else //没有子节点 
ExpandedSubBlockStart.gifContractedSubBlock.gif
                    {
                        
//进入下一个同层节点


                        
while (ColStack.Count > 0)
ExpandedSubBlockStart.gifContractedSubBlock.gif                        
{
                            en 
=
 ColStack.Peek();
                            
if (en.MoveNext()) //有同层节点

ExpandedSubBlockStart.gifContractedSubBlock.gif
                            {
                                
return true;//本次访问这个节点;


                            }

                            
else//没有同层节点
ExpandedSubBlockStart.gifContractedSubBlock.gif
                            {
                                ColStack.Pop().Dispose();
// 取消本层堆栈  

                            }


                        }

                        
return false//完全没有子节点 也没有下一个节点  就无法继续了
                    }




                }

                
else //第一次访问 直接返回
ExpandedSubBlockStart.gifContractedSubBlock.gif
                {
                    Started 
= true
;
                    
return
 cur.MoveNext();
                }


            }

            
return false;
        }


稍微包装一下 也实现一个IEumeratable 和扩展方法


ContractedBlock.gifExpandedBlockStart.gif外包装

    
public class RecursionEnumeratable<TItem> : IEnumerable<TItem>,IEnumerable <Stack<TItem >>
    {

            IEnumerable 
<TItem> objs ;
            Func
<TItem, IEnumerable<TItem>>
   ChildrenSelector;
            
public RecursionEnumeratable(TItem rootObject, Func<TItem, IEnumerable<TItem>>
 childrenSelector)
        {

     
            ChildrenSelector 
=
 childrenSelector;
             objs 
= new
 TItem[] { rootObject };
        

        }
            
public RecursionEnumeratable(IEnumerable<TItem> rootObjects, Func<TItem, IEnumerable<TItem>>
 childrenSelector)
        {
            objs
=
rootObjects;
      
            ChildrenSelector 
=
 childrenSelector;
  
        }
        

        
#region IEnumerable<TItem> Members


        
public IEnumerator<TItem> GetEnumerator()
        {
            
return new RecursionEnumerator<TItem>
(objs ,ChildrenSelector );
        }

        
#endregion


        
#region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            
return GetEnumerator();
        }

        
#endregion


        
#region IEnumerable<Stack<TItem>> Members

        IEnumerator
<Stack<TItem>> IEnumerable<Stack<TItem>>.GetEnumerator()
        {
            
return new RecursionEnumerator<TItem>
(objs, ChildrenSelector);
        }

        
#endregion

    }




    
public static class RecursionExtender
    {

        
static public RecursionEnumeratable<T> GetRecursionEnumeratable<T>(this T SingleRoot, Func<T, IEnumerable<T>>
 childrenSelector)
       {
           
return new RecursionEnumeratable<T>
(SingleRoot, childrenSelector);
       }


        
static public RecursionEnumeratable<T> GetRecursionEnumeratableAsRootCollection<T>(this IEnumerable<T> Roots, Func<T, IEnumerable<T>>
 childrenSelector)
        {
            
return new RecursionEnumeratable<T>
(Roots, childrenSelector);
            
        }



        


    }
就可以在任何你想要的对象上遍历了~

当然一个下午的工作一定会有很多纰漏 有什么意见不妨提出来 我修改修改




 

对于真正的懒人来说 以上内容全是乐色!

怎样名正言顺的偷懒才是重要的!

Sample  察看一个目录中所有子目录的文件列表
        static void Main(string [] args)
ExpandedBlockStart.gifContractedBlock.gif        
{
            
string RootDir= @"J:\Emule"
;
            

            
foreach (string str in RootDir.GetRecursionEnumeratable ( path=>
System.IO.Directory.GetDirectories (path) ))
ExpandedSubBlockStart.gifContractedSubBlock.gif            
{
                Console .WriteLine(str) ;
                System.IO.Directory.GetFiles (str).ToList().ForEach (s
=>
Console.WriteLine (s));
            
            }


            Console.ReadKey();
        }

或者干脆点
ContractedBlock.gifExpandedBlockStart.gifCode
            @"J:\Emule".GetRecursionEnumeratable(path => System.IO.Directory.GetDirectories(path))
        
        .ToList<string>
()
                .ForEach
                (
                    str =>

                    {
                        Console.WriteLine(str);
                        System.IO.Directory.GetFiles(str).ToList().ForEach(s 
=> Console.WriteLine(s));
                    }
                );



是不是比原来递归来递归去好一些呢?

有的时候  根节点不是一个对象 而是一个集合   比如treeview 的items
对于多个根节点  我也提供了支持








ExpandedBlockStart.gifContractedBlock.gif          (
new string[] @"J:\Emule" ,@"I:\cdimages"})
              
              .GetRecursionEnumeratableAsRootCollection (path 
=> System.IO.Directory.GetDirectories(path))
                .ToList
<string>()
                .ForEach
                (
                    str 
=>
ExpandedBlockStart.gifContractedBlock.gif                    
{
                        Console.WriteLine(str);
                        System.IO.Directory.GetFiles(str).ToList().ForEach(s 
=> Console.WriteLine(s));
                    }

                );




更复杂的状况  我们有时候不但要遍历节点  也要返回他们每一层的祖先列表   这里我同时实现了 IEnumeratable<Stack<TItem>>
Sample 2 从平面表中生成树

Table
ID  Name FatherID

            ItemEnt[] source
= new  ItemEnt[ 0 ];  //  从orm中读取表

            var roots 
=  source.Where(itm  =>  itm.FatherID  ==   0 );
            
// 多个根节点入口
            roots.GetRecursionEnumeratableAsRootCollection
                (
                
// 提供子节点方法
                    itm  =>  source.Where   
                        (
                            itmchild 
=>  itmchild.FatherID  ==  itm.ID
                        )
                )
                
// 返回每个节点及其祖先节点列表的方法
                .ToList  < Stack   <  ItemEnt >   > ()
                .ForEach 
                (stack
=> Console.WriteLine (   string .Join ( @" \ "  ,stack.Select (i => i.Name ).ToArray ()  )));

                    

            Console.ReadKey();


多简单~!


补充  鹤冲天同学提出了一个性能相关的很重要的问题  就是关于遍历的条件   老赵将其明确化 
“不过方法加predicate可以在中端就跳过一些节点,不用继续递归下去。”
如果仅仅对IEnumeratable 进行where 约束   一些本来不需要深入的子节点还是会继续深入下去的。 这样的确会带来性能的损失

这种where 约束分两部分 
1 是否选择本节点
2 是否对本节点的子节点今进行深入遍历

对于第一种  我在前面 为什么用枚举器中提到了  IEnumerable 的where 扩展是可以进行lazy filter的
对于第二中 我得补充下, 这个逻辑其实是 Children Selector 相关的。   可以在Children Selector中进行有效清晰的控制


ExpandedBlockStart.gif ContractedBlock.gif ( new   string []  @"J:\Emule" ,@"I:\cdimages"}
             
.GetRecursionEnumeratableAsRootCollection (path 
=>  (path.Length  < 80 ) ? System.IO.Directory.GetDirectories(path): new   string [ 0 ] ) 
             
.ToList
< string > () 
              
.ForEach 
              

                    
str 
=>  
                   
ExpandedBlockStart.gifContractedBlock.gif

                       
Console.WriteLine(str); 
                      
System.IO.Directory.GetFiles(str).ToList().ForEach(s 
=> Console.WriteLine(s)); 
                  
}
 
               
);

// 如果路径太长 超过80 那么就不进行深层遍历了

特别感谢 鹤冲天同学和老赵同学的质疑精神   例子写得太少  Use Case做到覆盖  不好意思!

请大家继续提意见~~


更新广度优先(感谢装配脑袋提供的思路)

广度优先本身不存在访问栈 所以在枚举祖先列表的时候有一定消耗
于是在这里 分别写了两个枚举器

ContractedBlock.gifExpandedBlockStart.gif祖先枚举器
   public class RecursionEnumeratorBreadthWithAncestors<TItem> :  System.Collections.IEnumerator, IEnumerator<IEnumerable <TItem>>
    {
        Func
<TItem, IEnumerable<TItem>> ChildrenSelector;




        
/// <summary>
        
/// 任务。对于一个节点的1层子节点遍历 叫做一个任务
        
/// </summary>
        struct TaskItem
        {
            
/// <summary>
            
/// 本次任务要遍历的所有子节点
            
/// </summary>
            public IEnumerator<TItem> CurrentTasks;
            
/// <summary>
            
/// 本次任务要遍历的父亲节点。因为不涉及 Pop() 这里只用list就可以了
            
/// </summary>
            public List<TItem> ParentList;


        }

        
bool started = false;
        Queue
<TaskItem> TaskQueue;
        IEnumerable
<TItem> roots;
        
public RecursionEnumeratorBreadthWithAncestors(IEnumerable<TItem> rootObjects, Func<TItem, IEnumerable<TItem>> childrenSelector)
        {
            ChildrenSelector 
= childrenSelector;
            TaskQueue 
= new Queue<TaskItem>();
            var ti 
= new TaskItem() { CurrentTasks = rootObjects.GetEnumerator(), ParentList = new List<TItem>() };
            TaskQueue.Enqueue(ti);
            roots 
= rootObjects;

        }
        IEnumerator
<TItem> CurrentEnum
        {
            
get
            {

                
return TaskQueue.Peek().CurrentTasks;
            }
        }


        
#region IEnumerator<TItem> Members

        
public TItem Current
        {
            
get { return TaskQueue.Peek().CurrentTasks.Current; }
        }

        
#endregion

        
#region IDisposable Members

        
public void Dispose()
        {
            TaskQueue.ToList().ForEach(s 
=> s.CurrentTasks.Dispose());
            TaskQueue 
= null;
            roots 
= null;
            ChildrenSelector 
= null;
        }

        
#endregion

        
#region IEnumerator Members

        
object IEnumerator.Current
        {
            
get { return Current; }
        }

        
public bool MoveNext()
        {
            
if (TaskQueue.Count > 0)
            {
                
if (started)
                {
                    
while (TaskQueue.Count>0)
                    { 
                        
if (CurrentEnum.MoveNext())
                        {
                            GainChildrenTask();
                            
return true;
                        }
                        
else
                        {
                            var t
=  TaskQueue.Dequeue();
                            t.CurrentTasks.Dispose();
                            t.ParentList 
= null;                         
                        
                        }
                    }
                    
return false;
                }
                
else
                {

                    started 
= true;
                    
if (CurrentEnum.MoveNext())
                    {
                        GainChildrenTask();
                        
return true;
                    }
                    
return false;
                }

            }




            
return false;
        }

        
void GainChildrenTask()
        {

            var cur 
= Current;
            TaskItem ti 
= new TaskItem()
             {
                 CurrentTasks 
= ChildrenSelector(cur).GetEnumerator(),
                 ParentList 
= TaskQueue.Peek().ParentList.Concat (new TItem[] { cur }).ToList()
             };

            TaskQueue.Enqueue(ti);

        }

        
public void Reset()
        {
            var rootsbak 
= roots;
            var bakSelector 
= ChildrenSelector;
            Dispose();
            started 
= false;
            ChildrenSelector 
= bakSelector;
            TaskQueue 
= new Queue<TaskItem>();
            var ti 
= new TaskItem() { CurrentTasks = rootsbak.GetEnumerator(), ParentList = new List<TItem>() };
            TaskQueue.Enqueue(ti);
        }

        
#endregion

        
#region IEnumerator<IEnumerable<TItem>> Members

        IEnumerable
<TItem> IEnumerator<IEnumerable<TItem>>.Current
        {
            
get 
            { 
                
return TaskQueue.Peek().ParentList.Concat( new TItem[]{ Current }).ToList () ; 
           
            }
        }

        
#endregion
    }
  

ContractedBlock.gifExpandedBlockStart.gif不取祖先的枚举器
 public class RecursionEnumeratorBreadth<TItem> : System.Collections.Generic.IEnumerator<TItem>, System.Collections.IEnumerator
    {
        Func
<TItem, IEnumerable<TItem>> ChildrenSelector;




        
bool started = false;
        Queue
<IEnumerator <TItem >> TaskQueue;
        IEnumerable
<TItem> roots;
        
public RecursionEnumeratorBreadth(IEnumerable<TItem> rootObjects, Func<TItem, IEnumerable<TItem>> childrenSelector  )
        {
            ChildrenSelector 
= childrenSelector;
            TaskQueue 
= new Queue<IEnumerator <TItem >>();
            var ti 
= rootObjects.GetEnumerator();
            TaskQueue.Enqueue(ti);
            roots 
= rootObjects;

        }
        IEnumerator
<TItem> CurrentEnum
        {
            
get
            {

                
return TaskQueue.Peek();
            }
        }


        
#region IEnumerator<TItem> Members

        
public TItem Current
        {
            
get { return TaskQueue.Peek().Current; }
        }

        
#endregion

        
#region IDisposable Members

        
public void Dispose()
        {
            TaskQueue.ToList().ForEach(s 
=> s.Dispose());
            TaskQueue 
= null;
            roots 
= null;
            ChildrenSelector 
= null;
        }

        
#endregion

        
#region IEnumerator Members

        
object IEnumerator.Current
        {
            
get { return Current; }
        }

        
public bool MoveNext()
        {
            
if (TaskQueue.Count > 0)
            {
                
if (started)
                {
                    
while (TaskQueue.Count>0)
                    { 
                        
if (CurrentEnum.MoveNext())
                        {
                            GainChildrenTask();
                            
return true;
                        }
                        
else
                        {
                            var t
=  TaskQueue.Dequeue();
                            t.Dispose();
                             
                        
                        }
                    }
                    
return false;
                }
                
else
                {

                    started 
= true;
                    
if (CurrentEnum.MoveNext())
                    {
                        GainChildrenTask();
                        
return true;
                    }
                    
return false;
                }

            }




            
return false;
        }

        
void GainChildrenTask()
        {

            var cur 
= Current;
            var ti 
=  ChildrenSelector(cur).GetEnumerator();
             

            TaskQueue.Enqueue(ti);

        }

        
public void Reset()
        {
            var rootsbak 
= roots;
            var bakSelector 
= ChildrenSelector;
            Dispose();
            started 
= false;
            ChildrenSelector 
= bakSelector;
            TaskQueue 
= new Queue<IEnumerator<TItem>>();
            var ti 
= rootsbak.GetEnumerator();
            TaskQueue.Enqueue(ti);
        }

        
#endregion


    }


外包装也作了小小的变化

ContractedBlock.gifExpandedBlockStart.gif外包装

public  class EmptyEnumerable<T>: IEnumerable<T> 
{
    IEnumerator
<T> Enumerator;
    
public EmptyEnumerable(IEnumerator<T> enumerator)
    {
        Enumerator 
= enumerator;
    
    }



    
#region IEnumerable<T> Members

    
public IEnumerator<T> GetEnumerator()
    {
        
return Enumerator;
    }

    
#endregion

    
#region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
        
return Enumerator;
    }

    
#endregion
}




    
public static class RecursionExtender
    {
        
public enum RecursionType
        { 
            DeepFirst
=0,
            Breadth
=1
        
        }

        
static public IEnumerable <T> GetRecursionEnumerable<T>(this T SingleRoot, Func<T, IEnumerable<T>> childrenSelector , RecursionType type)
        {
            

            
switch (type)
            { 
                
case RecursionType.DeepFirst :
                    
return new EmptyEnumerable <T>(new RecursionEnumeratorDepthFirst<T>(new T[] { SingleRoot }, childrenSelector));
                
case RecursionType.Breadth :
                    
return new EmptyEnumerable<T>(new RecursionEnumeratorBreadth <T>(new T[] { SingleRoot }, childrenSelector));
            }

            
return null;
        }


        
static public IEnumerable <IEnumerable <T> >GetRecursionEnumerableWithAncestors<T>(this T SingleRoot, Func<T, IEnumerable<T>> childrenSelector , RecursionType type)
        {
            

            
switch (type)
            { 
                
case RecursionType.DeepFirst :
                    
return new EmptyEnumerable <IEnumerable<T>>(new RecursionEnumeratorDepthFirst<T>(new T[] { SingleRoot }, childrenSelector));
                
case RecursionType.Breadth :
                    
return new EmptyEnumerable<IEnumerable<T>>(new RecursionEnumeratorBreadthWithAncestors <T>(new T[] { SingleRoot }, childrenSelector));
            }

            
return null;
        }



        
static public IEnumerable<T> GetRecursionEnumerableAsRootCollection<T>(this IEnumerable<T> Roots, Func<T, IEnumerable<T>> childrenSelector, RecursionType type)
        {
            
switch (type)
            {
                
case RecursionType.DeepFirst:
                    
return new EmptyEnumerable<T>(new RecursionEnumeratorDepthFirst<T>(Roots, childrenSelector));
                
case RecursionType.Breadth:
                    
return new EmptyEnumerable<T>(new RecursionEnumeratorBreadth<T>(Roots, childrenSelector));
            }

            
return null;
        }


        
static public IEnumerable<IEnumerable<T>> GetRecursionEnumerableAsRootCollectionWithAncestors<T>(this IEnumerable<T> Roots, Func<T, IEnumerable<T>> childrenSelector, RecursionType type)
        {


            
switch (type)
            {
                
case RecursionType.DeepFirst:
                    
return new EmptyEnumerable<IEnumerable<T>>(new RecursionEnumeratorDepthFirst<T>(Roots, childrenSelector));
                
case RecursionType.Breadth:
                    
return new EmptyEnumerable<IEnumerable<T>>(new RecursionEnumeratorBreadthWithAncestors<T>(Roots, childrenSelector));
            }

            
return null;
        }




    }


用法
ContractedBlock.gifExpandedBlockStart.gif用法
        static public IEnumerable<string> Rtest()
ExpandedBlockStart.gifContractedBlock.gif        
{
            
string dir = @"d:\webcast";
            var xx 
= dir.GetRecursionEnumeratable(s => System.IO.Directory.GetDirectories(s),RecursionExtender.RecursionType.DeepFirst );
            
return xx.ToList<string>();


        }


        
static public IEnumerable<IEnumerable <string>> Rtest2()
ExpandedBlockStart.gifContractedBlock.gif        
{
            
string dir = @"d:\webcast";
            var xx 
= dir.GetRecursionEnumeratableWithAncestors(s => System.IO.Directory.GetDirectories(s)
, RecursionExtender.RecursionType.DeepFirst);
            
return xx.ToList < IEnumerable<string>>();


        }



        
static public IEnumerable<string> R1test()
ExpandedBlockStart.gifContractedBlock.gif        
{
            
string dir = @"d:\webcast";
            var xx 
= dir.GetRecursionEnumeratable(s => System.IO.Directory.GetDirectories(s) ,RecursionExtender.RecursionType.Breadth );
            
return xx.ToList<string>();


        }


        
static public IEnumerable<IEnumerable<string>> R1test2()
ExpandedBlockStart.gifContractedBlock.gif        
{
            
string dir = @"d:\webcast";
            var xx 
= dir.GetRecursionEnumeratableWithAncestors(s => System.IO.Directory.GetDirectories(s), RecursionExtender.RecursionType.Breadth);
            
return xx.ToList<IEnumerable<string>>();


        }

    }


这个版本估计近期不会动了 :D

 


代码地址

RecursionEnumerator
http://files.cnblogs.com/waynebaby/RecursionEnumerator.zip

转载于:https://www.cnblogs.com/waynebaby/archive/2009/08/16/1546980.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值