学习 LLVM(20) ImutAVLTree 和 ImutAVLFactory
2012-03-18 00:00
274 查看
ImmutableSet 比较复杂,前面我们先了解了二叉查找树(binary search tree), AVL tree 才能理解其实现机制。这里先从其底层实现的 AVL 树节点,AVL 树操作工厂类开始,它们分别是 ImutAVLTree 和 ImutAVLFactory 。
写的时候使用了 MediaWiki 的语法写在 wiki 中,复制到 mediawiki 的页面中保存看,会更方便一些(并安装 syntaxhighlight 插件)。这里修改太费时间,实在是抱歉了。
参见:
* [[ADT]], [[ImutAVLFactory]], [[ImmutableSet]], [[ImmutableMap]]
* http://llvm.org/docs/ProgrammersManual.html#dss_immutableset
== ImutAVLTree ==
Immutable AVL-Tree:不可改变的 AVL 树的节点类。
<syntaxhighlight lang="cpp">
template <ImutInfo> class ImutAVLTree { // 注1: ImutInfo
ImutAVLFactory *factory // 工厂对象
ImutAVLTree *left, *right // 左节点, 右节点
ImutAVLTree *prev, *next // 实现 factory 中 Cache 的 hash 冲突处理。
uint height // 树高度
bool is_mutable // 标志:是否可变。新建的节点为可变的,完成整个树之后标记为不可变的。
bool is_digest_cached // 标志:是否已经生成了摘要(digest),摘要一旦生成就缓存起来,在 digest 字段中。
bool is_canonicalized // 标志:是否已经规范化了,这里规范化应是指加入到了 factory.Cache 中了。
T value // 节点保存的键和值。通过 ValInfoT trait 模板获取所需信息。
uint32 digest // 摘要
uint32 ref_count // 引用计数,计数减少到 0 的时候,会释放到 factory.freeNodes 队列(栈)中。
key_type, value_type, Factory, iterator 等类型的定义
friend 友类声明 ImutAVLFactory 等
getLeft(),getRight(),getHeight(),getValue() 得到树左、右节点、高度、节点值
getMaxElement() // 得到最大元素
find(),size(),begin(),end(),contains() 等容器方法
foreach() // 中序遍历(inorder traversal)
private this() // 私有构造,只能从工厂类 factory 中调用
retain(),release(),destroy() 等一些辅助方法。
}
</syntaxhighlight>
* 注1:在 ImmutableMap 中,使用了 ImutAVLTree,使用的模板参数为 ImutInfo = ImutKeyValueInfo<KeyT, ValueT>。参见 [[ImutKeyValueInfo]]。
== 实现机理 ==
=== find() ===
查找具有指定键值(key)的子树节点。如果符合条件的子树未找到,则返回 NULL。参见前面二叉查找树中 get() 方法的实现。
这是一个对二叉搜索树(binary search tree)的搜索实现:
* 1. 如果 search_key == 当前节点的 key,则找到了节点,返回。
* 2. 如果 search_key < 当前节点的 key,则查找左子树。
* 3. 否则 search_key > 当前节点的 key,则查找右子树。
* 4. 如果没有左子树、或右子树了,则返回 NULL,表示没找到。
=== foreach() ===
提供一个 functor 参数,其可通过 () 操作符调用(一般称这种 functor 为 callback),遍历这个树的每个节点/子树。遍历的顺序是 [[中序遍历]](inorder traversal)。
中序遍历顺序:先遍历左子树 left.foreach(),访问自己 callback(this),访问右子树 right.foreach()。这个函数可以容易地添加新的前序遍历和后续遍历版本的。
=== release() ===
ImutAVLTree 内部使用引用计数(refCount)来标记此节点被多少地方引用了。release() 方法的作用是引用计数-1,retain() 方法用来引用计数+1。当引用计数到达 0 的时候,表示这个节点不再被任何地方引用了,会调用 destroy() 方法。destroy() 方法中会分别释放左右子树,并请求 factory 释放此节点。
这里需要详细了解 factory 的功能才知道。参见对 [[ImutAVLFactory]] 的学习。
== 参见 ==
对ImutAVLTree 的构造等操作应该是在 factory 中进行的,参见 [[ImutAVLFactory]], [[ImmutableSet::Factory]], [[ImutIntervalAVLFactory]]。
该类为 [[ImutAVLTree]] 的工厂类。(Immutable AVL-Tree Factory class)
== ImutAVLFactory ==
<syntaxhighlight lang="cpp">
template <ImutInfoT> // 注1:ImutInfoT 缺省是 ImutContainerInfo<ValT>
class ImutAVLFactory {
typedef, friend ImutAVLTree Tree // 类型定义,友类定义。
// 拥有的数据。
CacheTy cache // 注2:CacheTy = DenseMap<uint,Tree*>
void* Allocator // 注3:实际类型为 BumpPtrAllocator
vector<Tree*> createdNodes // 已创建的节点队列。
vector<Tree*> freeNodes // 释放了、可用的节点队列。
this(), this(alloc) // 构造,如果给出 alloc,则外部拥有该 alloc。见注3
~this() // 不出意外会释放 alloc,如果拥有的话。
Tree* add(Tree *t, ValT &v) // 向树中添加新节点,参见实现机理部分。
Tree* remove(Tree *t, KeyT &k) // 从树中删除节点,参见实现机理部分。
Tree* getEmptyTree() // 得到空树。实际返回 NULL 指针作为空树。
incrementHeight() // 计算子树高度并+1
getHeight(TreeTy *) // 计算树高度。参见 ImutAVLTree::getHeight()
}
</syntaxhighlight>
* 注1:ImutInfoT 缺省是 ImutContainerInfo<ValT>,参见 [[ImutAVLTree]]和 [[ImutContainerInfo]] 的说明。
* 注2:CacheTy 在 typedef 区定义为 [[DenseMap]]。实现机理的时候详细参考。
* 注3:Allocator 的类型实际为 [[BumpPtrAllocator]],实现者使用 LSB(该指针的最低位) 作为对该分配器的拥有标志,因此改用普通指针类 *。然后提供了 getAllocator(), ownsAllocator() 方法以访问实际指针和标志。但是这样不方便调试。
* 注4:ImutAVLTree 将 height 直接存在节点中,这样不用每次计算?。
* 实际测量 sizeof() 是 36 字节。
== 实现机理 ==
[[ImutAVLTree]] 要实现的是平衡搜索二叉树,因此在插入(add)和删除(remove)的时候,要保持树的平衡性。关于 [[AVL]] 树的定义及理论,最好参见《数据结构》类型的教材。
=== add() ===
平衡二叉树的插入(insert)语义在 ImutAVLFactory 中实现为 add() 方法,其原型为:
Tree* add(Tree *t, ValT & v)
* 0. 实际调用 add_internal(t, v)
* 1. 创建新节点 createNode() 见下面的说明。
* 2. markImmutable(T) 标记节点为 Immutable 的。
* 3. recoverNodes() 暂时不明白什么意思。其会清空 createdNodes 队列但不知道何意?也许是回收不再使用的节点到 freeNodes 中?
* 4. getCanonicalTree() 将节点放入 Cache 中。
add_internal 实现
* if key == T.key 则值相同的节点存在,创建新节点使具有新值并返回。
* key < T.key 递归调用 add_internal(key, T.left),插入到左子树,并 balanceTree()。关于 balanceTree() 详见下面的数明。
* key > T.key 递归调用 add_internal(key, T.right),插入到右子树,并 balanceTree()
=== balanceTree() ===
在 add_internal(), remove_internal() 中使用,以平衡新创建的树。
但是我实际调试的时候,发现其并没有按照 AVL 要求的 bf>1 (bf=|hr-hl|) 的要求进行平衡。而是当 bf>2 的时候才进行平衡,可是这是红黑树的平衡方式吗?或者这样可以有效地减少旋转次数吗?
add_internal() 和 balanceTree() 结合起来有一个有趣的特性,就是当需要改变任何节点信息时,都会创建一个新的树节点,这符合 Immutable(不可改变的) 这个词的语义要求。例如下面的例子:
<syntaxhighlight lang="cpp">
typedef ImmutableSet<int> IntSet; // 定义一个存整数 不可变集合
typedef ImmutableSet<int>::Factory FactT; // 定义该集合的工厂类
FactT fac; // 实例化一个工厂对象,后续产生集合、修改都用工厂对象。
// 实测:sizeof(IntSet::TreeTy)=36, sizeof(IntSet)=4, sizeof(FactT)=64
IntSet empty = fac.getEmptySet(); // 构造一个空集合。
IntSet a = fac.add(empty, 7); // 构造一个含有集合 (7)
IntSet b = fac.add(a, 5); // 集合为 (7 left=5)
IntSet c = fac.add(b, 3); // 集合为 (7 left=(5 left=3)), 注1
IntSet d = fac.add(c, 1); // 集合为 (5 left=(3 l=1) r=7), 注2
d.inorder_foreach(cout << value); // 输出为 1 3 5 7
</syntaxhighlight>
* 注1:按照标准 AVL 树定义,这里要进行一次 LL 旋转,变成结构为 (5 l=3 r=7)。可实际实现未进行旋转。
* 注2:这里进行了 LL 旋转,此时 bf=2。
在上面创建 a, b, c, d 的过程中,会为每次插入新节点产生几个新的 TreeTy 节点(恰好在从根到该插入点的查找路径上)。实际最后的效果是,a、b、c、d 自己不会发生变化,所有变化都会用产生新的节点来完成,所以叫做 ImmutableSet。这种特定的语义要求,一定是和使用者那里的要求有关的。反之,我们在语法书上看到的 AVLTree 都是会在 add, remove 的时候,改变树的结构和节点字段的。
但是为实现 balanceTree() 功能,需要在树中保存和维护 height 字段,实际为了各种功能,TreeTy 的大小达到了 36 个字节(在保存 int 的数据时)。而一般 AVLTreeNode 可能只需要 16 个字节。而且 add_internal() 实现为递归调用,我们是否可以尝试不用递归的实现呢?
=== remove() ===
remove() 的实现与 add() 有相似之处,调用 remove_internal() 实际完成删除,markImmutable(), recoverNodes() 和 add() 同。
* 1. 如果要删除的 key == T.key, 则返回为 combineTrees(left, right)
* 2. key < T.key, 则递归 remove_internal(key, T.left), 然后 balanceTree()
* 3. key > T.key 则递归 remove_internal(key, T.right), 然后 balanceTree()
balanceTree() 上面已经详细说明了,下面说明 combineTrees(L, R),L,R 表示要删除的节点的左右子节点:
* 与 [[binary search tree]] 类似,如果 L,R 有一者为 null 则返回另一者;可能两者都为 null,则返回既为 null。
* 选择右子树的最左(最小)子节点,作为新的合并之后的根节点。参见前述 BST 树 delete 的说明。实际实现在 removeMinBinding() 函数中,其实现方式为递归调用。
* 合并之后的树,进行 balanceTree() 操作。
上述过程中,任何删除、平衡操作都会产生新的 TreeTy 节点,以实现 immutable 语义。因为保存了树的 height 信息,因此 balanceTree() 能够根据左右子树的 height 进行调整。同样的问题是,如果没有 height 字段而是 bf 字段,不用递归,能实现 remove() 和 balance() 吗?
=== createNode() ===
这个函数用于创建一个新的树节点,并指定其左子树、右子树、节点值。其注释似乎与代码不符合。实际代码实现中:
* 首先从 freeNodes 队列中查找是否有回收的可用节点,如果有则弹出一个可用的。
* 否则,使用 [[BumpPtrAllocator]] 分配器分配一个新节点。
* 使用 in-place new 构造节点,使其具有 left,right,value,height 等值。
* 将新创建的节点,放置到 createdNodes 队列中。
这里实际上 freeNodes 是当做"栈"的形式使用的。freeNodes 在节点析构的时候被加入进来。
=== markImmutable() ===
标记指定节点及其所有子节点为 Immutable(不可改变的) 标志。一般是新建的节点需要标记。
=== getCanonicalTree() ===
为指定的树节点(及其子节点)计算一个摘要(digest)做为 key,在 Cache 中查找,其中 Cache 是一个 DenseMap(HashMap)。树节点使用 next, prev 字段构成双向链表,以能够解决放置在 cache 产生的 digest key 冲突问题。
一个树节点放置到 Cache 中即被设置为 Canonicalized 标志。如果在 Cache 中找到了内容相同的树,则返回找到的,释放新建的那个。
写的时候使用了 MediaWiki 的语法写在 wiki 中,复制到 mediawiki 的页面中保存看,会更方便一些(并安装 syntaxhighlight 插件)。这里修改太费时间,实在是抱歉了。
ImutAVLTree
定义在文件 llvm/include/llvm/[[ADT]]/ImmutableSet.h 中。参见:
* [[ADT]], [[ImutAVLFactory]], [[ImmutableSet]], [[ImmutableMap]]
* http://llvm.org/docs/ProgrammersManual.html#dss_immutableset
== ImutAVLTree ==
Immutable AVL-Tree:不可改变的 AVL 树的节点类。
<syntaxhighlight lang="cpp">
template <ImutInfo> class ImutAVLTree { // 注1: ImutInfo
ImutAVLFactory *factory // 工厂对象
ImutAVLTree *left, *right // 左节点, 右节点
ImutAVLTree *prev, *next // 实现 factory 中 Cache 的 hash 冲突处理。
uint height // 树高度
bool is_mutable // 标志:是否可变。新建的节点为可变的,完成整个树之后标记为不可变的。
bool is_digest_cached // 标志:是否已经生成了摘要(digest),摘要一旦生成就缓存起来,在 digest 字段中。
bool is_canonicalized // 标志:是否已经规范化了,这里规范化应是指加入到了 factory.Cache 中了。
T value // 节点保存的键和值。通过 ValInfoT trait 模板获取所需信息。
uint32 digest // 摘要
uint32 ref_count // 引用计数,计数减少到 0 的时候,会释放到 factory.freeNodes 队列(栈)中。
key_type, value_type, Factory, iterator 等类型的定义
friend 友类声明 ImutAVLFactory 等
getLeft(),getRight(),getHeight(),getValue() 得到树左、右节点、高度、节点值
getMaxElement() // 得到最大元素
find(),size(),begin(),end(),contains() 等容器方法
foreach() // 中序遍历(inorder traversal)
private this() // 私有构造,只能从工厂类 factory 中调用
retain(),release(),destroy() 等一些辅助方法。
}
</syntaxhighlight>
* 注1:在 ImmutableMap 中,使用了 ImutAVLTree,使用的模板参数为 ImutInfo = ImutKeyValueInfo<KeyT, ValueT>。参见 [[ImutKeyValueInfo]]。
== 实现机理 ==
=== find() ===
查找具有指定键值(key)的子树节点。如果符合条件的子树未找到,则返回 NULL。参见前面二叉查找树中 get() 方法的实现。
这是一个对二叉搜索树(binary search tree)的搜索实现:
* 1. 如果 search_key == 当前节点的 key,则找到了节点,返回。
* 2. 如果 search_key < 当前节点的 key,则查找左子树。
* 3. 否则 search_key > 当前节点的 key,则查找右子树。
* 4. 如果没有左子树、或右子树了,则返回 NULL,表示没找到。
=== foreach() ===
提供一个 functor 参数,其可通过 () 操作符调用(一般称这种 functor 为 callback),遍历这个树的每个节点/子树。遍历的顺序是 [[中序遍历]](inorder traversal)。
中序遍历顺序:先遍历左子树 left.foreach(),访问自己 callback(this),访问右子树 right.foreach()。这个函数可以容易地添加新的前序遍历和后续遍历版本的。
=== release() ===
ImutAVLTree 内部使用引用计数(refCount)来标记此节点被多少地方引用了。release() 方法的作用是引用计数-1,retain() 方法用来引用计数+1。当引用计数到达 0 的时候,表示这个节点不再被任何地方引用了,会调用 destroy() 方法。destroy() 方法中会分别释放左右子树,并请求 factory 释放此节点。
这里需要详细了解 factory 的功能才知道。参见对 [[ImutAVLFactory]] 的学习。
== 参见 ==
对ImutAVLTree 的构造等操作应该是在 factory 中进行的,参见 [[ImutAVLFactory]], [[ImmutableSet::Factory]], [[ImutIntervalAVLFactory]]。
ImutAVLFactory
该类定义在文件 llvm/include/llvm/[[ADT]]/[[ImmutableSet.h]] 中。该类为 [[ImutAVLTree]] 的工厂类。(Immutable AVL-Tree Factory class)
== ImutAVLFactory ==
<syntaxhighlight lang="cpp">
template <ImutInfoT> // 注1:ImutInfoT 缺省是 ImutContainerInfo<ValT>
class ImutAVLFactory {
typedef, friend ImutAVLTree Tree // 类型定义,友类定义。
// 拥有的数据。
CacheTy cache // 注2:CacheTy = DenseMap<uint,Tree*>
void* Allocator // 注3:实际类型为 BumpPtrAllocator
vector<Tree*> createdNodes // 已创建的节点队列。
vector<Tree*> freeNodes // 释放了、可用的节点队列。
this(), this(alloc) // 构造,如果给出 alloc,则外部拥有该 alloc。见注3
~this() // 不出意外会释放 alloc,如果拥有的话。
Tree* add(Tree *t, ValT &v) // 向树中添加新节点,参见实现机理部分。
Tree* remove(Tree *t, KeyT &k) // 从树中删除节点,参见实现机理部分。
Tree* getEmptyTree() // 得到空树。实际返回 NULL 指针作为空树。
incrementHeight() // 计算子树高度并+1
getHeight(TreeTy *) // 计算树高度。参见 ImutAVLTree::getHeight()
}
</syntaxhighlight>
* 注1:ImutInfoT 缺省是 ImutContainerInfo<ValT>,参见 [[ImutAVLTree]]和 [[ImutContainerInfo]] 的说明。
* 注2:CacheTy 在 typedef 区定义为 [[DenseMap]]。实现机理的时候详细参考。
* 注3:Allocator 的类型实际为 [[BumpPtrAllocator]],实现者使用 LSB(该指针的最低位) 作为对该分配器的拥有标志,因此改用普通指针类 *。然后提供了 getAllocator(), ownsAllocator() 方法以访问实际指针和标志。但是这样不方便调试。
* 注4:ImutAVLTree 将 height 直接存在节点中,这样不用每次计算?。
* 实际测量 sizeof() 是 36 字节。
== 实现机理 ==
[[ImutAVLTree]] 要实现的是平衡搜索二叉树,因此在插入(add)和删除(remove)的时候,要保持树的平衡性。关于 [[AVL]] 树的定义及理论,最好参见《数据结构》类型的教材。
=== add() ===
平衡二叉树的插入(insert)语义在 ImutAVLFactory 中实现为 add() 方法,其原型为:
Tree* add(Tree *t, ValT & v)
* 0. 实际调用 add_internal(t, v)
* 1. 创建新节点 createNode() 见下面的说明。
* 2. markImmutable(T) 标记节点为 Immutable 的。
* 3. recoverNodes() 暂时不明白什么意思。其会清空 createdNodes 队列但不知道何意?也许是回收不再使用的节点到 freeNodes 中?
* 4. getCanonicalTree() 将节点放入 Cache 中。
add_internal 实现
* if key == T.key 则值相同的节点存在,创建新节点使具有新值并返回。
* key < T.key 递归调用 add_internal(key, T.left),插入到左子树,并 balanceTree()。关于 balanceTree() 详见下面的数明。
* key > T.key 递归调用 add_internal(key, T.right),插入到右子树,并 balanceTree()
=== balanceTree() ===
在 add_internal(), remove_internal() 中使用,以平衡新创建的树。
但是我实际调试的时候,发现其并没有按照 AVL 要求的 bf>1 (bf=|hr-hl|) 的要求进行平衡。而是当 bf>2 的时候才进行平衡,可是这是红黑树的平衡方式吗?或者这样可以有效地减少旋转次数吗?
add_internal() 和 balanceTree() 结合起来有一个有趣的特性,就是当需要改变任何节点信息时,都会创建一个新的树节点,这符合 Immutable(不可改变的) 这个词的语义要求。例如下面的例子:
<syntaxhighlight lang="cpp">
typedef ImmutableSet<int> IntSet; // 定义一个存整数 不可变集合
typedef ImmutableSet<int>::Factory FactT; // 定义该集合的工厂类
FactT fac; // 实例化一个工厂对象,后续产生集合、修改都用工厂对象。
// 实测:sizeof(IntSet::TreeTy)=36, sizeof(IntSet)=4, sizeof(FactT)=64
IntSet empty = fac.getEmptySet(); // 构造一个空集合。
IntSet a = fac.add(empty, 7); // 构造一个含有集合 (7)
IntSet b = fac.add(a, 5); // 集合为 (7 left=5)
IntSet c = fac.add(b, 3); // 集合为 (7 left=(5 left=3)), 注1
IntSet d = fac.add(c, 1); // 集合为 (5 left=(3 l=1) r=7), 注2
d.inorder_foreach(cout << value); // 输出为 1 3 5 7
</syntaxhighlight>
* 注1:按照标准 AVL 树定义,这里要进行一次 LL 旋转,变成结构为 (5 l=3 r=7)。可实际实现未进行旋转。
* 注2:这里进行了 LL 旋转,此时 bf=2。
在上面创建 a, b, c, d 的过程中,会为每次插入新节点产生几个新的 TreeTy 节点(恰好在从根到该插入点的查找路径上)。实际最后的效果是,a、b、c、d 自己不会发生变化,所有变化都会用产生新的节点来完成,所以叫做 ImmutableSet。这种特定的语义要求,一定是和使用者那里的要求有关的。反之,我们在语法书上看到的 AVLTree 都是会在 add, remove 的时候,改变树的结构和节点字段的。
但是为实现 balanceTree() 功能,需要在树中保存和维护 height 字段,实际为了各种功能,TreeTy 的大小达到了 36 个字节(在保存 int 的数据时)。而一般 AVLTreeNode 可能只需要 16 个字节。而且 add_internal() 实现为递归调用,我们是否可以尝试不用递归的实现呢?
=== remove() ===
remove() 的实现与 add() 有相似之处,调用 remove_internal() 实际完成删除,markImmutable(), recoverNodes() 和 add() 同。
* 1. 如果要删除的 key == T.key, 则返回为 combineTrees(left, right)
* 2. key < T.key, 则递归 remove_internal(key, T.left), 然后 balanceTree()
* 3. key > T.key 则递归 remove_internal(key, T.right), 然后 balanceTree()
balanceTree() 上面已经详细说明了,下面说明 combineTrees(L, R),L,R 表示要删除的节点的左右子节点:
* 与 [[binary search tree]] 类似,如果 L,R 有一者为 null 则返回另一者;可能两者都为 null,则返回既为 null。
* 选择右子树的最左(最小)子节点,作为新的合并之后的根节点。参见前述 BST 树 delete 的说明。实际实现在 removeMinBinding() 函数中,其实现方式为递归调用。
* 合并之后的树,进行 balanceTree() 操作。
上述过程中,任何删除、平衡操作都会产生新的 TreeTy 节点,以实现 immutable 语义。因为保存了树的 height 信息,因此 balanceTree() 能够根据左右子树的 height 进行调整。同样的问题是,如果没有 height 字段而是 bf 字段,不用递归,能实现 remove() 和 balance() 吗?
=== createNode() ===
这个函数用于创建一个新的树节点,并指定其左子树、右子树、节点值。其注释似乎与代码不符合。实际代码实现中:
* 首先从 freeNodes 队列中查找是否有回收的可用节点,如果有则弹出一个可用的。
* 否则,使用 [[BumpPtrAllocator]] 分配器分配一个新节点。
* 使用 in-place new 构造节点,使其具有 left,right,value,height 等值。
* 将新创建的节点,放置到 createdNodes 队列中。
这里实际上 freeNodes 是当做"栈"的形式使用的。freeNodes 在节点析构的时候被加入进来。
=== markImmutable() ===
标记指定节点及其所有子节点为 Immutable(不可改变的) 标志。一般是新建的节点需要标记。
=== getCanonicalTree() ===
为指定的树节点(及其子节点)计算一个摘要(digest)做为 key,在 Cache 中查找,其中 Cache 是一个 DenseMap(HashMap)。树节点使用 next, prev 字段构成双向链表,以能够解决放置在 cache 产生的 digest key 冲突问题。
一个树节点放置到 Cache 中即被设置为 Canonicalized 标志。如果在 Cache 中找到了内容相同的树,则返回找到的,释放新建的那个。
相关文章推荐
- 马哥教育面授班20-2第一周学习笔记4
- C++学习笔记20,复制构造函数
- 【学习】动态树 link cut tree
- LLVM每日谈之十一 编译器相关学习资料推荐
- Java与Flex学习笔记(20)---将flex页面嵌入到jsp页面中
- Bootstrap学习总结笔记(20)-- 基本插件之Alert警告框
- Bootstrap基本插件学习笔记之Alert警告框(20)
- linux shell 脚本攻略学习16--wc命令详解,tree命令详解
- 我的OpenCV学习笔记(20):提取元素的轮廓及形状描述子
- 2016/20/27学习工作日志
- 机器学习小组知识点20:EM算法(Expectation - Maximization)
- Jquery LigerUI框架学习(二)之Tree于Tab标签实现iframe功能
- python socket 的使用 - 千月的python linux 系统管理指南学习笔记(20)
- 学习 LLVM(2) isa<> 模板函数
- Programming Ability Test学习 1064. Complete Binary Search Tree (30)
- ArcGIS API for JavaScript 4.2学习笔记[20] 使用缓冲区结合Query对象进行地震点查询【重温异步操作思想】
- 学习笔记-extjs treepanel
- android ViewTreeObserver中文翻译学习
- vue+element的tree组件学习模板
- Spring学习心得(20)--aop的需求分析:错误异常分析