zoukankan      html  css  js  c++  java
  • C# 通用树形数据结构

    前言

        树在图论中是一种重要的图,由于其自身的许多特殊性质,也是一种重要的计算机数据结构,在很多地方都有用。但是这些树大多都是作为其他应用的内部数据结构来使用。我们无法了解这些树的详细信息,而 .Net 也没有在内置的集合类库中提供树形数据结构的类。很多时候我们都需要树形数据完成一些工作,在自己的实践经验和查阅大量相关资料后,我编写了一个使用简单,能方便地将普通集合转换为树形集合(当然前提是这些集合元素之间存在能够表明层级关系的数据),提供了大量图论中有关节点和整棵树的信息和常用算法的通用树形数据结构类。希望能够简化此类需求的开发。

    正文

        图论中有关树的定义和性质都能很容易查到资料,就不再啰嗦,进入正题。首先,定义一个树形结构有很多方法,在此我选用最直观的双亲孩子表示法定义。为了能将包含层级信息的普通数据转换为树形数据,使用接口进行定义,并以接口为核心进行扩展。接口定义如下:

     1     /// <summary>
     2     /// 可分层数据接口
     3     /// </summary>
     4     /// <typeparam name="T">数据类型</typeparam>
     5     public interface IHierarchical<out T>
     6     {
     7         /// <summary>
     8         /// 当前节点的数据
     9         /// </summary>
    10         T Current { get; }
    11 
    12         /// <summary>
    13         /// 根节点
    14         /// </summary>
    15         IHierarchical<T> Root { get; }
    16 
    17         /// <summary>
    18         /// 双亲(父)节点
    19         /// </summary>
    20         IHierarchical<T> Parent { get; }
    21 
    22         /// <summary>
    23         /// 祖先节点集合(按路径距离升序)
    24         /// </summary>
    25         IEnumerable<IHierarchical<T>> Ancestors { get; }
    26 
    27         /// <summary>
    28         /// 子节点集合
    29         /// </summary>
    30         IEnumerable<IHierarchical<T>> Children { get; }
    31 
    32         /// <summary>
    33         /// 后代节点集合(深度优先先序遍历)
    34         /// </summary>
    35         IEnumerable<IHierarchical<T>> Descendants { get; }
    36 
    37         /// <summary>
    38         /// 兄弟节点集合(不包括自身节点)
    39         /// </summary>
    40         IEnumerable<IHierarchical<T>> Siblings { get; }
    41 
    42         /// <summary>
    43         /// 在兄弟节点中的排行
    44         /// </summary>
    45         int IndexOfSiblings { get; }
    46 
    47         /// <summary>
    48         /// 节点的层
    49         /// </summary>
    50         int Level { get; }
    51 
    52         /// <summary>
    53         /// 节点(以当前节点为根的子树)的高度
    54         /// </summary>
    55         int Height { get; }
    56 
    57         /// <summary>
    58         /// 节点的度
    59         /// </summary>
    60         int Degree { get; }
    61 
    62         /// <summary>
    63         /// 树(以当前节点为根的子树)的所有节点中度最大的节点的度
    64         /// </summary>
    65         int MaxDegreeOfTree { get; }
    66 
    67         /// <summary>
    68         /// 当前节点是否是根节点
    69         /// </summary>
    70         bool IsRoot { get; }
    71 
    72         /// <summary>
    73         /// 当前节点是否是叶子节点
    74         /// </summary>
    75         bool IsLeaf { get; }
    76 
    77         /// <summary>
    78         /// 当前节点是否有子节点
    79         /// </summary>
    80         bool HasChild { get; }
    81 
    82         /// <summary>
    83         /// 以当前节点为根返回树形排版的结构字符串
    84         /// </summary>
    85         /// <param name="formatter">数据对象格式化器(内容要为一行,否则会影响排版)</param>
    86         /// <param name="convertToSingleLine">处理掉换行符变成单行文本</param>
    87         /// <returns></returns>
    88         string ToString(Func<T, string> formatter, bool convertToSingleLine = false);
    89     }

        然后根据接口定义扩展类型。实现如下:

      1     /// <summary>
      2     /// 可分层数据扩展
      3     /// </summary>
      4     public static class HierarchicalExtensions
      5     {
      6         /// <summary>
      7         /// 转换为可分层数据
      8         /// </summary>
      9         /// <typeparam name="T">数据类型</typeparam>
     10         /// <param name="item">数据</param>
     11         /// <param name="childSelector">下层数据选择器</param>
     12         /// <returns>已分层的数据</returns>
     13         public static IHierarchical<T> AsHierarchical<T>(this T item, Func<T, IEnumerable<T>> childSelector)
     14         {
     15             return new Hierarchical<T>(item, childSelector);
     16         }
     17 
     18         /// <summary>
     19         /// 分层数据
     20         /// </summary>
     21         /// <typeparam name="T">数据类型</typeparam>
     22         private class Hierarchical<T> : IHierarchical<T>
     23         {
     24             private readonly object _locker;
     25             private readonly Func<T, IEnumerable<T>> _childSelector;
     26             private IEnumerable<IHierarchical<T>> _children;
     27 
     28             /// <summary>
     29             /// 实例化分层数据
     30             /// </summary>
     31             /// <param name="item">数据</param>
     32             /// <param name="childSelector">下层数据选择器</param>
     33             public Hierarchical(T item, Func<T, IEnumerable<T>> childSelector)
     34             {
     35                 _locker = new object();
     36                 _children = null;
     37                 Current = item;
     38                 _childSelector = childSelector;
     39             }
     40 
     41             /// <summary>
     42             /// 实例化分层数据
     43             /// </summary>
     44             /// <param name="item">数据</param>
     45             /// <param name="parent">上层数据</param>
     46             /// <param name="childSelector">下层数据选择器</param>
     47             private Hierarchical(T item, IHierarchical<T> parent, Func<T, IEnumerable<T>> childSelector)
     48                 : this(item, childSelector)
     49             {
     50                 Parent = parent;
     51             }
     52 
     53             /// <summary>
     54             /// 初始化下层节点集合
     55             /// </summary>
     56             /// <returns>迭代结果集合接口</returns>
     57             private IEnumerable<IHierarchical<T>> InitializeChildren()
     58             {
     59                 var children = _childSelector(Current);
     60                 if (children == null)
     61                     yield break;
     62 
     63                 foreach (T item in children)
     64                 {
     65                     yield return new Hierarchical<T>(item, this, _childSelector);
     66                 }
     67             }
     68 
     69             #region IHierarchicalDataItem<T> 成员
     70 
     71             public T Current { get; }
     72 
     73             public IHierarchical<T> Root
     74             {
     75                 get
     76                 {
     77                     IHierarchical<T> node = this;
     78 
     79                     while (node.Parent != null)
     80                         node = node.Parent;
     81 
     82                     return node;
     83                 }
     84             }
     85 
     86             public IHierarchical<T> Parent { get; }
     87 
     88             public IEnumerable<IHierarchical<T>> Ancestors
     89             {
     90                 get
     91                 {
     92                     IHierarchical<T> node = Parent;
     93 
     94                     while (node != null)
     95                     {
     96                         yield return node;
     97                         node = node.Parent;
     98                     }
     99                 }
    100             }
    101 
    102             public IEnumerable<IHierarchical<T>> Children
    103             {
    104                 get
    105                 {
    106                     if (_children == null)
    107                         lock (_locker)
    108                             if (_children == null)
    109                                 _children = InitializeChildren().ToArray();
    110 
    111                     return _children;
    112                 }
    113             }
    114 
    115             public IEnumerable<IHierarchical<T>> Descendants
    116             {
    117                 get
    118                 {
    119                     Stack<IHierarchical<T>> stack = new Stack<IHierarchical<T>>(Children.Reverse());
    120 
    121                     while (stack.Count > 0)
    122                     {
    123                         IHierarchical<T> node = stack.Pop();
    124                         yield return node;
    125 
    126                         foreach (IHierarchical<T> child in node.Children.Reverse())
    127                         {
    128                             stack.Push(child);
    129                         }
    130                     }
    131                 }
    132             }
    133 
    134             public IEnumerable<IHierarchical<T>> Siblings => Parent?.Children?.Where(node => node != this);
    135 
    136             public int IndexOfSiblings
    137             {
    138                 get
    139                 {
    140                     if (Parent == null) return 0;
    141 
    142                     int index = 0;
    143                     foreach (var child in Parent.Children)
    144                     {
    145                         if (child == this) break;
    146                         index++;
    147                     }
    148 
    149                     return index;
    150                 }
    151             }
    152 
    153             //无缓存方法,每次访问相同节点都会重新枚举数据源并生成结果对象
    154             //包含相同数据T的包装IHierarchical<T>每次都不一样
    155             //public IEnumerable<IHierarchical<T>> Children
    156             //{
    157             //    get
    158             //    {
    159             //        var children = _childSelector(Current);
    160             //        if (children == null)
    161             //            yield break;
    162 
    163             //        foreach (T item in children)
    164             //        {
    165             //            yield return new Hierarchical<T>(item, this, _childSelector);
    166             //        }
    167             //    }
    168             //}
    169 
    170             public int Level => Parent?.Level + 1 ?? 1;
    171 
    172             public int Height => (Descendants.Any() ? Descendants.Select(node => node.Level).Max() - Level : 0) + 1;
    173 
    174             public int Degree => Children?.Count() ?? 0;
    175 
    176             public int MaxDegreeOfTree => Math.Max(Degree, Descendants.Any() ? Descendants.Select(node => node.Degree).Max() : 0);
    177 
    178             public bool IsRoot => Parent == null;
    179 
    180             public bool IsLeaf => Degree == 0;
    181 
    182             public bool HasChild => !IsLeaf;
    183 
    184             public string ToString(Func<T, string> formatter, bool convertToSingleLine = false)
    185             {
    186                 var sbr = new StringBuilder();
    187                 sbr.AppendLine(convertToSingleLine
    188                     ? formatter(Current).Replace("
    ", @"
    ").Replace("
    ", @"
    ")
    189                     : formatter(Current));
    190 
    191                 var sb = new StringBuilder();
    192                 foreach (var node in Descendants)
    193                 {
    194                     sb.Append(convertToSingleLine
    195                         ? formatter(node.Current).Replace("
    ", @"
    ").Replace("
    ", @"
    ")
    196                         : formatter(node.Current));
    197                     sb.Insert(0, node == node.Parent.Children.Last() ? "└─" : "├─");
    198 
    199                     for (int i = 0; i < node.Ancestors.Count() - (Ancestors?.Count() ?? 0) - 1; i++)
    200                     {
    201                         sb.Insert(0,
    202                             node.Ancestors.ElementAt(i) == node.Ancestors.ElementAt(i + 1).Children.Last()
    203                                 ? "    "
    204                                 : "");
    205                     }
    206 
    207                     sbr.AppendLine(sb.ToString());
    208                     sb.Clear();
    209                 }
    210 
    211                 return sbr.ToString();
    212             }
    213 
    214             public override string ToString()
    215             {
    216                 return ToString(node => node.ToString());
    217             }
    218 
    219             #endregion
    220         }
    221 
    222         /// <summary>
    223         /// 获取从根到节点的路径(中顺序经过的节点集合)
    224         /// </summary>
    225         /// <typeparam name="T">数据类型</typeparam>
    226         /// <param name="node">节点</param>
    227         /// <returns>路径中按经过顺序组成的节点集合</returns>
    228         public static IEnumerable<IHierarchical<T>> GetPathFromRoot<T>(this IHierarchical<T> node) =>
    229             node.Ancestors.Reverse();
    230 
    231         /// <summary>
    232         /// 判断节点是否是指定节点的祖先节点
    233         /// </summary>
    234         /// <typeparam name="T">节点数据类型</typeparam>
    235         /// <param name="node">待判断的节点</param>
    236         /// <param name="target">目标节点</param>
    237         /// <returns>判断结果</returns>
    238         public static bool IsAncestorOf<T>(this IHierarchical<T> node, IHierarchical<T> target)
    239         {
    240             if (node.Root != target.Root)
    241                 throw new InvalidOperationException($"{nameof(node)} and {nameof(target)} are not at same tree.");
    242 
    243             return target.Ancestors.SingleOrDefault(n => n == node) != null;
    244         }
    245 
    246         /// <summary>
    247         /// 判断节点是否是指定节点的后代节点
    248         /// </summary>
    249         /// <typeparam name="T">节点数据类型</typeparam>
    250         /// <param name="node">待判断的节点</param>
    251         /// <param name="target">目标节点</param>
    252         /// <returns>判断结果</returns>
    253         public static bool IsDescendantOf<T>(this IHierarchical<T> node, IHierarchical<T> target)
    254         {
    255             if (node.Root != target.Root)
    256                 throw new InvalidOperationException($"{nameof(node)} and {nameof(target)} are not at same tree.");
    257 
    258             return target.IsAncestorOf(node);
    259         }
    260 
    261         /// <summary>
    262         /// 获取节点与指定节点的最近公共祖先节点
    263         /// </summary>
    264         /// <typeparam name="T">节点数据类型</typeparam>
    265         /// <param name="node">待查找的节点</param>
    266         /// <param name="target">目标节点</param>
    267         /// <returns>最近的公共祖先节点</returns>
    268         public static IHierarchical<T> GetNearestCommonAncestor<T>(this IHierarchical<T> node, IHierarchical<T> target)
    269         {
    270             if (node.Root != target.Root)
    271                 throw new InvalidOperationException($"{nameof(node)} and {nameof(target)} are not at same tree.");
    272 
    273             if (node.IsAncestorOf(target)) return node;
    274             if (target.IsAncestorOf(node)) return target;
    275 
    276             return node.Ancestors.Intersect(target.Ancestors).OrderByDescending(no => no.Level).First();
    277         }
    278 
    279         /// <summary>
    280         /// 获取从指定节点到当前节点的路径
    281         /// </summary>
    282         /// <typeparam name="T">节点数据类型</typeparam>
    283         /// <param name="node">当前节点(终点)</param>
    284         /// <param name="from">目标节点(起点)</param>
    285         /// <returns>按从目标节点到当前节点顺序经过的节点集合</returns>
    286         public static IEnumerable<IHierarchical<T>> GetPathFromNode<T>(this IHierarchical<T> node,
    287             IHierarchical<T> from)
    288         {
    289             if(node.Root != from.Root)
    290                 throw new InvalidOperationException($"{nameof(node)} and {nameof(from)} are not at same tree.");
    291 
    292             yield return from;
    293 
    294             if (node == from) yield break;
    295 
    296             if (node.IsAncestorOf(from))
    297             {
    298                 foreach (var ancestor in from.Ancestors)
    299                 {
    300                     yield return ancestor;
    301                     if (ancestor == node)
    302                     {
    303                         yield break;
    304                     }
    305                 }
    306             }
    307 
    308             var ancestorsOfNode = node.Ancestors.ToArray();
    309             if (node.IsDescendantOf(from))
    310             {
    311                 for (int i = Array.IndexOf(ancestorsOfNode, from) - 1; i >= 0; i--)
    312                 {
    313                     yield return ancestorsOfNode[i];
    314                 }
    315 
    316                 yield return node;
    317                 yield break;
    318             }
    319 
    320             var keyNode = ancestorsOfNode.Intersect(from.Ancestors).OrderByDescending(no => no.Level).First();
    321             foreach (var ancestor in from.Ancestors)
    322             {
    323                 yield return ancestor;
    324                 if (ancestor == keyNode)
    325                 {
    326                     break;
    327                 }
    328             }
    329 
    330             for (int i = Array.IndexOf(ancestorsOfNode, keyNode) - 1; i >= 0; i--)
    331             {
    332                 yield return ancestorsOfNode[i];
    333             }
    334 
    335             yield return node;
    336         }
    337 
    338         /// <summary>
    339         /// 获取从当前节点到指定节点的路径
    340         /// </summary>
    341         /// <typeparam name="T">节点数据类型</typeparam>
    342         /// <param name="node">当前节点(起点)</param>
    343         /// <param name="to">目标节点(终点)</param>
    344         /// <returns>按从当前节点到目标节点顺序经过的节点集合</returns>
    345         public static IEnumerable<IHierarchical<T>> GetPathToNode<T>(this IHierarchical<T> node, IHierarchical<T> to)
    346         {
    347             if (node.Root != to.Root)
    348                 throw new InvalidOperationException($"{nameof(node)} and {nameof(to)} are not at same tree.");
    349 
    350             return to.GetPathFromNode(node);
    351         }
    352 
    353         /// <summary>
    354         /// 获取子孙数据(深度优先,先序)
    355         /// </summary>
    356         /// <typeparam name="T">数据类型</typeparam>
    357         /// <param name="root"></param>
    358         /// <param name="predicate">子孙筛选条件</param>
    359         /// <returns>筛选的子孙</returns>
    360         public static IEnumerable<IHierarchical<T>> GetDescendantsDfsDlr<T>(this IHierarchical<T> root,
    361             Func<IHierarchical<T>, bool> predicate = null)
    362         {
    363             var children = predicate == null ? root.Children : root.Children.Where(predicate);
    364             Stack<IHierarchical<T>> stack = new Stack<IHierarchical<T>>(children.Reverse());
    365 
    366             while (stack.Count > 0)
    367             {
    368                 IHierarchical<T> node = stack.Pop();
    369                 yield return node;
    370 
    371                 children = predicate == null ? node.Children : node.Children.Where(predicate);
    372                 foreach (IHierarchical<T> child in children.Reverse())
    373                 {
    374                     stack.Push(child);
    375                 }
    376             }
    377 
    378             #region 递归方式
    379 
    380             //foreach (T t in childSelector(root))
    381             //{
    382             //    if (predicate(t))
    383             //        yield return t;
    384             //    foreach (T child in GetDescendantDfsDlr(t, childSelector, predicate))
    385             //        yield return child;
    386             //}
    387 
    388             #endregion 递归方式
    389         }
    390 
    391         /// <summary>
    392         /// 获取子孙数据(深度优先,后序)
    393         /// </summary>
    394         /// <typeparam name="T">数据类型</typeparam>
    395         /// <param name="root"></param>
    396         /// <param name="predicate">子孙筛选条件</param>
    397         /// <returns>筛选的子孙</returns>
    398         public static IEnumerable<IHierarchical<T>> GetDescendantsDfsLrd<T>(this IHierarchical<T> root,
    399             Func<IHierarchical<T>, bool> predicate = null)
    400         {
    401             var children = predicate == null ? root.Children : root.Children.Where(predicate);
    402             Stack<IHierarchical<T>> stack = new Stack<IHierarchical<T>>(children.Reverse());
    403 
    404             IHierarchical<T> lastAccessedNode = null;
    405 
    406             while (stack.Count > 0)
    407             {
    408                 var node = stack.Peek();
    409 
    410                 if (node.Children.Any() && node.Children.Last() != lastAccessedNode)
    411                 {
    412                     children = predicate == null ? node.Children : node.Children.Where(predicate);
    413                     foreach (IHierarchical<T> child in children.Reverse())
    414                     {
    415                         stack.Push(child);
    416                     }
    417                 }
    418                 else
    419                 {
    420                     yield return node;
    421 
    422                     lastAccessedNode = node;
    423                     stack.Pop();
    424                 }
    425             }
    426         }
    427 
    428         /// <summary>
    429         /// 获取子孙数据(广度优先)
    430         /// </summary>
    431         /// <typeparam name="T">数据类型</typeparam>
    432         /// <param name="root"></param>
    433         /// <param name="predicate">子孙筛选条件</param>
    434         /// <returns>筛选的子孙</returns>
    435         public static IEnumerable<IHierarchical<T>> GetDescendantsBfs<T>(this IHierarchical<T> root,
    436             Func<IHierarchical<T>, bool> predicate = null)
    437         {
    438             predicate = predicate ?? (t => true);
    439             Queue<IHierarchical<T>> queue = new Queue<IHierarchical<T>>(root.Children.Where(predicate));
    440 
    441             while (queue.Count > 0)
    442             {
    443                 IHierarchical<T> node = queue.Dequeue();
    444                 yield return node;
    445 
    446                 foreach (IHierarchical<T> child in node.Children.Where(predicate))
    447                 {
    448                     queue.Enqueue(child);
    449                 }
    450             }
    451         }
    452 
    453         /// <summary>
    454         /// 转换为可枚举集合
    455         /// </summary>
    456         /// <typeparam name="T">数据类型</typeparam>
    457         /// <param name="root"></param>
    458         /// <param name="predicate">子孙筛选条件</param>
    459         /// <param name="enumerateType">枚举方式</param>
    460         /// <returns>已枚举的集合</returns>
    461         public static IEnumerable<IHierarchical<T>> AsEnumerable<T>(this IHierarchical<T> root,
    462             EnumerateType enumerateType = EnumerateType.DfsDlr,
    463             Func<IHierarchical<T>, bool> predicate = null)
    464         {
    465             switch (enumerateType)
    466             {
    467                 case EnumerateType.DfsDlr:
    468                     yield return root;
    469 
    470                     foreach (var descendant in GetDescendantsDfsDlr(root, predicate))
    471                     {
    472                         yield return descendant;
    473                     }
    474 
    475                     break;
    476                 case EnumerateType.DfsLrd:
    477                     foreach (var descendant in GetDescendantsDfsLrd(root, predicate))
    478                     {
    479                         yield return descendant;
    480                     }
    481 
    482                     yield return root;
    483 
    484                     break;
    485                 case EnumerateType.Bfs:
    486                     yield return root;
    487 
    488                     foreach (var descendant in GetDescendantsBfs(root, predicate))
    489                     {
    490                         yield return descendant;
    491                     }
    492                     
    493                     break;
    494                 default:
    495                     throw new ArgumentOutOfRangeException(nameof(enumerateType), enumerateType, null);
    496             }
    497         }
    498     }

        里面需要用到一个枚举,定义如下:

     1     /// <summary>
     2     /// 枚举方式
     3     /// </summary>
     4     public enum EnumerateType : byte
     5     {
     6         /// <summary>
     7         /// 深度优先,先序
     8         /// </summary>
     9         DfsDlr,
    10 
    11         /// <summary>
    12         /// 深度优先,后序
    13         /// </summary>
    14         DfsLrd,
    15 
    16         /// <summary>
    17         /// 广度优先(层序)
    18         /// </summary>
    19         Bfs
    20     }

        这个实现类是一个不可变的树形数据类,一旦生成,无法修改,主要是作为普通数据的转换包装使用。但是其中的Children属性是延迟加载的,只有第一次访问Children属性或其他直接或间接依赖Children属性的成员时才会初始化Children属性,所以如果在访问到Children之前修改了源数据集,修改是会反映到Children中的。这个实现的遍历算法都采用了循环法,而非递归法,能有效避免栈溢出错误,运行效率也更好。

        使用public static IHierarchical<T> AsHierarchical<T>(this T item, Func<T, IEnumerable<T>> childSelector)扩展方法就可以获取根节点的引用。其中childSelector是一个委托,用于获取源数据集中每个节点的子节点集合。

        GetDescendants系列扩展方法可以以指定的遍历方式获取某个节点后代节点(不包括自身节点),并且其中的Func<IHierarchical<T>, bool> predicate参数可以对后代节点进行筛选,只有符合条件的才会返回,类似Linq中的Where方法,不过要注意,如果某个节点不符合条件,那么那个节点的后代也会被过滤掉。

        AsEnumerable方法和GetDescendants系列方法差不多,不过这个方法会遍历包括自身在内的整棵树,这个方法同样可以进行过滤,注意点也和GetDescendants系列方法一样。

        GetPath系列扩展方法可以对同一颗树的两个节点进行路径查找,返回的集合按顺序是从起点到终点所经过的所有节点,包括起点和终点。

        IsXxxOf系列扩展方法可以判断同一颗树的两个节点是否存在祖先后代关系。

        GetNearestCommonAncestor扩展方法可以查找同一颗树两个节点的最近公共祖先节点,如果两个节点间存在祖先后代关系则返回祖先节点。

        string ToString(Func<T, string> formatter, bool convertToSingleLine = false)接口方法可以生成类似 Windows cmd 命令 tree 生成的目录树样式的字符串,方便总览树的结构。Func<T, string> formatter委托用于获取数据T的字符串表示形式, bool convertToSingleLine 用于指定是否要强行把获得的字符串中的换行符处理掉,避免排版混乱。

        结语

        至此,一个通用的树形结构类就大功告成,图论中有关树的信息和常用算法基本上已经包括在内。数据集只要逻辑上包含层次关系就可以轻松包装成树形数据,除了不能修改这棵树这个小问题。自定义实现 IHierarchical<out T> 接口的类可以实现包含特殊功能的树形数据类。如果想起来能增加什么功能的话,会在Github上更新代码。

        转载请完整保留以下内容,未经授权删除以下内容进行转载盗用的,保留追究法律责任的权利!

        本文地址:https://www.cnblogs.com/coredx/p/10517448.html

        完整源代码(包含示例):Github

        里面有各种小东西,这只是其中之一,不嫌弃的话可以Star一下。

  • 相关阅读:
    document.body.clientHeight 和 document.documentElement.clientHeight 的区别
    Javascript操作div及其内含对象示例
    面向对象分析设计的经验原则
    翻页控件示例代码
    C#的6种常用集合类示例
    UML基础知识
    重温OSI和TCP/IP网络分层
    设计模式总结
    活用设计模式
    GridView当数据源为空时仍显示表头
  • 原文地址:https://www.cnblogs.com/coredx/p/10517448.html
Copyright © 2011-2022 走看看