本文首发于我的公众号 Linux云计算网络(id: cloud_dev) ,专注于干货分享,号内有 10T 书籍和视频资源,后台回复 「1024」 即可领取,欢迎大家关注,二维码文末可以扫。

一、高级数据结构

  本章以后到第21章(并查集)隶属于高级数据结构的内容。前面还留了两章:贪心算法和摊还分析,打算后面再来补充。之前的章节讨论的支持动态数据集上的操作,如查找、插入、删除等都是基于简单的线性表、链表和树等结构,本章以后的部分在原来更高的层次上来讨论这些操作,更高的层次意味着更复杂的结构,但更低的时间复杂度(包括摊还时间)。

  • B树是为磁盘存储还专门设计的平衡查找树。因为磁盘操作的速度要远远慢于内存,所以度量B树的性能,不仅要考虑动态集合操作消耗了多少计算时间,还要考虑这些操作执行了多少次磁盘存储。因此,B树被设计成尽量减少磁盘访问的次数。知道了这一点,就会明白B树的变形B+树了,B+树通过将数据存储在叶子节点从而增大了一个节点所包含的信息,进而更加减少了磁盘的访问次数。
  • 可合并堆:支持make-heap, insert, minimum, extract-min, union这5种操作。在堆排序章节讨论过二叉堆,除了union操作,二叉堆的性能都很好。该部分讨论的二项堆和斐波那契堆对union操作能够获得很好的性能,此外,对于其他操作,也能获得较好的改进。
  • 该部分提出一种数据结构:van Emde Boas树,当关键字在有限范围内的整数时,进一步改进了动态集合操作的性能,可以在O(lglgu)时间内完成。
  • 不相交集合(并查集):通过一棵简单的有根树来表示每个集合,就可以得到惊人的快速操作:一个由m个操作构成的序列的运行时间为O(n&(n)),而对于宇宙中的原子数总和n,&(n)也<=4,所以可以认为实际时间是O(n)。

二、B树

  从历史演进上来看,B树是在2-3树的基础上演变而来,2-3树是一种类型的平衡查找树,AVL树的平衡条件是“保证任意节点的左右子树的高度差不超过1”,而红黑树则是“通过对节点着不同的颜色来约束平衡”,2-3树则是“通过约束内部节点的度来达到平衡”:分为普通两个度的节点和三个度的节点,故名为2-3树,如下图所示:

  

  更深一步,从实现原理上看,红黑树是2-3树的一种简单实现,原因在于2-3树在编码实现上比较复杂,且失去二叉树的特性,不易被人接受和理解。如果稍加对2-3树做一点转换,就可以变为二叉树,做法是:用两种连线来区分度为3和度为2的节点,比如用红色的线来连接度为3的节点,黑色的线连接普通的节点,以这种方法,即可将2-3树转化为红黑树。如下:

  2-3树是将内部节点赋予2-3度来达到平衡,那更一般地,自然想到为内部节点赋予更大大小的度,进而减小了树的高度,应对更多不同的场景。从这个层面上看,B树是在前人的基础上应运而生的一种树结构。

  从应用场景来看,在一些大规模的数据存储中,如数据库,分布式系统等,实现索引查询这样一个实际背景下,数据的访问经常需要进行磁盘的读写操作,这个时候的瓶颈主要就在于磁盘的I/O上。如果采用普通的二叉查找树结构,由于树的深度过大,会造成磁盘I/O的读写过于频繁,进而导致访问效率低下(一般树的一个节点对应一个磁盘的页面,读取一次磁盘页相当于访问无数次内存)。那么,如何减少树的深度,一个基本的、很自然的想法就是:采用多叉树结构。节点的分支因子越大(可以理解成节点的孩子节点数),树的高度也就越低,从而查询的效率也就越高。从这个意义上来看,就有了B树的结构。

  前面提到过,在大多数系统中,B树算法的运行时间主要由它所执行的disk-read和disk-write操作的次数所决定的,其余时间在内存中计算,速度不在一个量级。因此,应该有效地使用这两种操作,即让它们读取更多的信息以更少的次数。由于这个原因,在B树中,一个节点的大小通常相当于一个完整的磁盘页。因此,一个B树节点可以拥有的孩子数就由磁盘页的大小决定。理论上说,孩子数越多越好,因为这样树的高度会减少,查询效率会增加,但要保证一个节点的总大小不能大于磁盘中的一个页的大小,否则在一个节点内操作时还要来回访问内存,反而拖慢效率。

三、B树的定义及动态集合操作

一棵B树具有以下的性质:

1)每个节点x有三个属性:

  a、x.n—>关键字个数

  b、关键字递增排序

  c、x.leaf—>节点是否属于叶子节点

2)每个节点有x.n+1个孩子节点

3)每个节点关键字 > 其左孩子节点 < 其右孩子节点

4)每个叶子节点具有相同的深度,即树的高度h。

5)每个节点用最小度数 t 来表示其关键字个数的上下界,或者孩子节点(分支因子)的个数的上下界。一般,每个非根节点中所包含的关键字个数 j 满足:

t-1 <= j <= 2*t - 1

根节点至少包括一个关键字,若非叶子节点,则至少两个分子,即 t>= 2。

  与红黑树相比,虽然两者的高度都以 O(lgn)的速度增长,但对于 B 树来说底要大很多倍。对大多数的树的操作来说,要查找的结点数在 B 树中要比红黑树中少大约 lgt 的因子。因为在树中查找任意一个结点通常需要一次磁盘存取,所以磁盘存取的次数大大的减少了。

以下代码表示B树中的一个节点:

 /// B树中的一个结点
struct BTreeNode
{
vector<int> Keys;
vector<BTreeNode *> Childs;
BTreeNode *Parent;///< 父结点。当该结点是树的根结点时,Parent结点为nullptr
bool IsLeaf; ///< 是否为叶子结点 BTreeNode() : Parent( nullptr ), IsLeaf( true ) {} size_t KeysSize()
{
return Keys.size();
}
};

  关于B树的动态集合操作,就不一一述说了,《算法导论》书已经讲得非常清楚了,而且图文并茂,照着认真看,绝对是没问题的。下面是实现的代码:

#ifndef _B_TREE_H_
#define _B_TREE_H_ #include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <sstream>
#include <cassert> using namespace std; class BTree
{
public:
/// B树中的一个结点
struct BTreeNode
{
vector<int> Keys;
vector<BTreeNode *> Childs;
BTreeNode *Parent; ///< 父结点。当该结点是树的根结点时,Parent结点为nullptr
bool IsLeaf; ///< 是否为叶子结点 BTreeNode() : Parent( nullptr ), IsLeaf( true ) {} size_t KeysSize()
{
return Keys.size();
}
}; /// 构造一棵最小度为t的B树(t>=2)
BTree( int t ) : _root( nullptr ), _t( t )
{
assert( t >= );
} ~BTree()
{
_ReleaseNode( _root );
} /// @brief B树的查找操作
///
/// 在B-树中查找给定关键字的方法类似于二叉排序树上的查找。
/// 不同的是在每个结点上确定向下查找的路径不一定是二路而是keynum+1路的。\n
/// 实现起来还是相当容易的!
pair<BTreeNode *, size_t> Search( int key )
{
return _SearchInNode( _root, key );
} /// @brief 插入一个值的操作
///
/// 这里没有使用《算法导论》里介绍的一趟的方法,而是自己想象出来的二趟的方法
/// 效率肯定不如书上介绍的一趟优美,但是能解决问题。\n
/// 因为插入操作肯定是在叶子结点上进行的,首先顺着书向下走直到要进行插入操作的叶子结点将新值插入到该叶子结点中去.
/// 如果因为这个插入操作而使用该结点的值的个数>2*t-1的上界,就需要递归向上进行分裂操作。
/// 如果分裂到了根结点,还要处理树长高的情况。\n
bool Insert( int new_key )
{
if ( _root == nullptr ) //空树
{
_root = new BTreeNode();
_root->IsLeaf = true;
_root->Keys.push_back( new_key );
return true;
} if ( Search( new_key ).first == nullptr ) //是否已经存在该结点
{
BTreeNode *node = _root;
while ( !node->IsLeaf )
{
int index = ;
while ( index < node->Keys.size() && new_key >= node->Keys[index] )
{
++index;
}
node = node->Childs[index];
} //插入到Keys里去
node->Keys.insert( find_if( node->Keys.begin(), node->Keys.end(), bind2nd( greater<int>(), new_key ) ), new_key ); //再递归向上处理结点太大的情况
while ( node->KeysSize() > * _t - )
{
//=====开始分裂======
int prove_node_key = node->Keys[node->KeysSize() / - ]; // 要提升的结点的key //后半部分成为一个新节点
BTreeNode *new_node = new BTreeNode();
new_node->IsLeaf = node->IsLeaf;
new_node->Keys.insert( new_node->Keys.begin(), node->Keys.begin() + node->KeysSize() / , node->Keys.end() );
new_node->Childs.insert( new_node->Childs.begin(), node->Childs.begin() + node->Childs.size() / , node->Childs.end() );
assert( new_node->Childs.empty() || new_node->Childs.size() == new_node->Keys.size() + );
for_each( new_node->Childs.begin(), new_node->Childs.end(), [&]( BTreeNode * c )
{
c->Parent = new_node;
} ); //把后半部分从原来的节点中删除
node->Keys.erase( node->Keys.begin() + node->KeysSize() / - , node->Keys.end() );
node->Childs.erase( node->Childs.begin() + node->Childs.size() / , node->Childs.end() );
assert( node->Childs.empty() || node->Childs.size() == node->Keys.size() + ); BTreeNode *parent_node = node->Parent;
if ( parent_node == nullptr ) //分裂到了根结点,树要长高了,需要NEW一个结点出来
{
parent_node = new BTreeNode();
parent_node->IsLeaf = false;
parent_node->Childs.push_back( node );
_root = parent_node;
}
node->Parent = new_node->Parent = parent_node; auto insert_pos = find_if( parent_node->Keys.begin(), parent_node->Keys.end(), bind2nd( greater<int>(), prove_node_key ) ) - parent_node->Keys.begin();
parent_node->Keys.insert( parent_node->Keys.begin() + insert_pos, prove_node_key );
parent_node->Childs.insert( parent_node->Childs.begin() + insert_pos + , new_node ); node = parent_node;
} return true;
}
return false;
} /// @brief 删除一个结点的操作
bool Delete( int key_to_del )
{
auto found_node = Search( key_to_del );
if ( found_node.first == nullptr ) //找不到值为key_to_del的结点
{
return false;
} if ( !found_node.first->IsLeaf ) //当要删除的结点不是叶子结点时用它的前驱来替换,再删除它的前驱
{
//前驱
BTreeNode *previous_node = found_node.first->Childs[found_node.second];
while ( !previous_node->IsLeaf )
{
previous_node = previous_node->Childs[previous_node->Childs.size() - ];
} //替换
found_node.first->Keys[found_node.second] = previous_node->Keys[previous_node->Keys.size() - ];
found_node.first = previous_node;
found_node.second = previous_node->Keys.size() - ;
} //到这里,found_node一定是叶子结点
assert( found_node.first->IsLeaf );
_DeleteLeafNode( found_node.first, found_node.second ); return true;
} private:
void _ReleaseNode( BTreeNode *node )
{
for_each( node->Childs.begin(), node->Childs.end(), [&]( BTreeNode * c )
{
_ReleaseNode( c );
} );
delete node;
} /// @brief 删除B树中的一个叶子结点
///
/// @param node 要删除的叶子结点!
/// @param index 要删除的叶子结点上的第几个值
/// @note 必须保证传入的node结点为叶子结点
void _DeleteLeafNode( BTreeNode *node, size_t index )
{
assert( node && node->IsLeaf ); if ( node == _root )
{
//要删除的值在根结点上,并且此时根结点也是叶子结点,因为本方法被调用时要保证node参数是叶子结点
_root->Keys.erase( _root->Keys.begin() + index );
if ( _root->Keys.empty() )
{
//成为了一棵空B树
delete _root;
_root = nullptr;
}
return;
} //以下是非根结点的情况 if ( node->Keys.size() > _t - )
{
//要删除的结点中Key的数目>t-1,因此再-1也不会打破B树的性质
node->Keys.erase( node->Keys.begin() + index );
}
else //会打破平衡
{
//是否借到了一个顶点
bool borrowed = false; //试着从左兄弟借一个结点
BTreeNode *left_brother = _GetLeftBrother( node );
if ( left_brother && left_brother->Keys.size() > _t - )
{
int index_in_parent = _GetIndexInParent( left_brother );
BTreeNode *parent = node->Parent; node->Keys.insert( node->Keys.begin(), parent->Keys[index_in_parent] );
parent->Keys[index_in_parent] = left_brother->Keys[left_brother->KeysSize() - ];
left_brother->Keys.erase( left_brother->Keys.end() - ); ++index;
borrowed = true;
}
else
{
//当左兄弟借不到时,试着从右兄弟借一个结点
BTreeNode *right_brother = _GetRightBrother( node );
if ( right_brother && right_brother->Keys.size() > _t - )
{
int index_in_parent = _GetIndexInParent( node );
BTreeNode *parent = node->Parent; node->Keys.push_back( parent->Keys[index_in_parent] );
parent->Keys[index_in_parent] = right_brother->Keys[];
right_brother->Keys.erase( right_brother->Keys.begin() ); borrowed = true;
}
} if ( borrowed )
{
//因为借到了结点,所以可以直接删除结点
_DeleteLeafNode( node, index );
}
else
{
//左右都借不到时先删除再合并
node->Keys.erase( node->Keys.begin() + index );
_UnionNodes( node );
}
}
} /// @brief node找一个相邻的结点进行合并
///
/// 优先选取左兄弟结点,再次就选择右兄弟结点
void _UnionNodes( BTreeNode * node )
{
if ( node )
{
if ( node == _root ) //node是头结点
{
if ( _root->Keys.empty() )
{
//头结点向下移动一级,此时树的高度-1
_root = _root->Childs[];
_root->Parent = nullptr; delete node;
return;
}
}
else
{
if ( node->KeysSize() < _t - )
{
BTreeNode *left_brother = _GetLeftBrother( node );
if ( left_brother == nullptr )
{
left_brother = _GetRightBrother( node );
swap( node, left_brother );
} //与左兄弟进行合并
int index_in_parent = _GetIndexInParent( left_brother );
node->Keys.insert( node->Keys.begin(), node->Parent->Keys[index_in_parent] );
node->Parent->Keys.erase( node->Parent->Keys.begin() + index_in_parent );
node->Parent->Childs.erase( node->Parent->Childs.begin() + index_in_parent + );
left_brother->Keys.insert( left_brother->Keys.end(), node->Keys.begin(), node->Keys.end() );
left_brother->Childs.insert( left_brother->Childs.begin(), node->Childs.begin(), node->Childs.end() );
for_each( left_brother->Childs.begin(), left_brother->Childs.end(), [&]( BTreeNode * c )
{
c->Parent = left_brother;
} ); delete node;
_UnionNodes( left_brother->Parent );
}
}
}
} pair<BTreeNode *, size_t> _SearchInNode( BTreeNode *node, int key )
{
if ( !node )
{
//未找到,树为空的情况
return make_pair( static_cast<BTreeNode *>( nullptr ), );
}
else
{
int index = ;
while ( index < node->Keys.size() && key >= node->Keys[index] )
{
if ( key == node->Keys[index] )
{
return make_pair( node, index );
}
else
{
++index;
}
} if ( node->IsLeaf )
{
//已经找到根了,不能再向下了未找到
return make_pair( static_cast<BTreeNode *>( nullptr ), );
}
else
{
return _SearchInNode( node->Childs[index], key );
}
}
} void _GetDotLanguageViaNodeAndEdge( stringstream &ss, BTreeNode *node )
{
if ( node && !node->Keys.empty() )
{
int index = ;
ss << " node" << node->Keys[] << "[label = \"";
while ( index < node->Keys.size() )
{
ss << "<f" << * index << ">|";
ss << "<f" << * index + << ">" << node->Keys[index] << "|";
++index;
}
ss << "<f" << * index << ">\"];" << endl;; if ( !node->IsLeaf )
{
for( int i = ; i < node->Childs.size(); ++i )
{
BTreeNode *c = node->Childs[i];
ss << " \"node" << node->Keys[] << "\":f" << * i << " -> \"node" << c->Keys[] << "\":f" << ( * c->Keys.size() + ) / << ";" << endl;
}
} for_each( node->Childs.begin(), node->Childs.end(), [&]( BTreeNode * c )
{
_GetDotLanguageViaNodeAndEdge( ss, c );
} );
}
} /// 得到一个结点的左兄弟结点,如果不存在左兄弟结点则返回nullptr
BTreeNode * _GetLeftBrother( BTreeNode *node )
{
if ( node && node->Parent )
{
BTreeNode *parent = node->Parent;
for ( int i = ; i < parent->Childs.size(); ++i )
{
if ( parent->Childs[i] == node )
{
return parent->Childs[i - ];
}
}
}
return nullptr;
} /// 得到一个结点的右兄弟结点,如果不存在右兄弟结点则返回nullptr
BTreeNode * _GetRightBrother( BTreeNode *node )
{
if ( node && node->Parent )
{
BTreeNode *parent = node->Parent;
for ( int i = ; i < static_cast<int>( parent->Childs.size() ) - ; ++i )
{
if ( parent->Childs[i] == node )
{
return parent->Childs[i + ];
}
}
}
return nullptr;
} /// 得到一个结点在其父结点中属于第几个子结点
/// @return 返回-1时表示错误
int _GetIndexInParent( BTreeNode *node )
{
assert( node && node->Parent ); for ( int i = ; i < node->Parent->Childs.size(); ++i )
{
if ( node->Parent->Childs[i] == node )
{
return i;
}
} return -;
} BTreeNode *_root; ///< B树的根结点指针
int _t; ///< B树的 最小度数。即所有的结点的Keys的个数应该t-1 <= n <= 2t-1,除了根结点可以最少为1个Key
}; #endif//_B_TREE_H_

四、B树的引申——B+树、B*树

B+树是对B树的一种变形树,它与B树的差异在于:

  • 有k个子结点的结点必然有k个关键码;
  • 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
  • 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。

B树和B+树各有优缺点:

  • B+树的磁盘读写代价更低:B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一磁盘页中,那么一页所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
  • 访问缓存命中率高:其一,B+树在内部节点上不含数据项,因此关键字存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据项也具有更好的缓存命中率;其二,B+树的叶子结点都是相链的,因此对整棵树的遍历只需要一次线性遍历叶子结点即可。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
  • B+树的查询效率更加稳定:由于非叶子节点只是充当叶子结点中数据项的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

当然,B树也不是因此就没有优点,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。

由于B+树较好的访问性能,一般,B+树比B 树更适合实际应用中操作系统的文件索引和数据库索引!

  B*树则是在B+树的基础上,又新增了一项规定:内部节点新增指向兄弟节点的指针。另外,B*树定义了非叶子结点关键字个数至少为(2/3)*t,即块的最低使用率为2/3(代替B+树的1/2);B*树在分裂节点时,由于可以向空闲较多的兄弟节点进行转移,因此其空间利用率更高。


我的公众号 「Linux云计算网络」(id: cloud_dev),号内有 10T 书籍和视频资源,后台回复 「1024」 即可领取,分享的内容包括但不限于 Linux、网络、云计算虚拟化、容器Docker、OpenStack、Kubernetes、工具、SDN、OVS、DPDK、Go、Python、C/C++编程技术等内容,欢迎大家关注。

 

算法导论第十八章 B树的更多相关文章

  1. 算法导论笔记——第十八章 B树

    18.1 B树的定义  18.2 B树的基本操作 与一棵二叉搜索树一样,可以在从树根到叶子这个单程向下过程中将一个新的关键字插入B树中.为了做到这一点,当沿着树向下查找新的关键字所属位置时,就分裂沿途 ...

  2. 浅谈算法和数据结构: 十 平衡查找树之B树

    前面讲解了平衡查找树中的2-3树以及其实现红黑树.2-3树种,一个节点最多有2个key,而红黑树则使用染色的方式来标识这两个key. 维基百科对B树的定义为“在计算机科学中,B树(B-tree)是一种 ...

  3. 转 浅谈算法和数据结构: 十 平衡查找树之B树

    前面讲解了平衡查找树中的2-3树以及其实现红黑树.2-3树种,一个节点最多有2个key,而红黑树则使用染色的方式来标识这两个key. 维基百科对B树的定义为"在计算机科学中,B树(B-tre ...

  4. 算法导论 第十二章 二叉搜索树(python)

    上图: 这是二叉搜索树(也有说是查找树的)基本结构:如果y是x的左子树中的一个结点,那么y.key <= x.key(如a图中的6根结点大于它左子树的每一个结点 6 >= {2,5,5}) ...

  5. 浅谈算法和数据结构: 七 二叉查找树 八 平衡查找树之2-3树 九 平衡查找树之红黑树 十 平衡查找树之B树

    http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html 前文介绍了符号表的两种实现,无序链表和有序数组,无序链表在插入的 ...

  6. B树——算法导论(25)

    B树 1. 简介 在之前我们学习了红黑树,今天再学习一种树--B树.它与红黑树有许多类似的地方,比如都是平衡搜索树,但它们在功能和结构上却有较大的差别. 从功能上看,B树是为磁盘或其他存储设备设计的, ...

  7. "《算法导论》之‘树’":二叉查找树

    树的介绍部分摘取自博文二叉查找树(一).二叉查找树(二).二叉查找树. 1. 树的介绍 1.1 树的定义 树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合. 把它叫做“ ...

  8. 算法导论:Trie字典树

    1. 概述 Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树. Trie一词来自retrieve,发音为/tr ...

  9. 《Linux内核设计与实现》课本第十八章自学笔记——20135203齐岳

    <Linux内核设计与实现>课本第十八章自学笔记 By20135203齐岳 通过打印来调试 printk()是内核提供的格式化打印函数,除了和C库提供的printf()函数功能相同外还有一 ...

随机推荐

  1. Android Message里传送的数据[转]

    package org.hualang.handlertest; import android.app.Activity; import android.os.Bundle; import andro ...

  2. jenkins2 插件安装

    文章来自:http://www.ciandcd.com 文中的代码来自可以从github下载: https://github.com/ciandcd Jenkins的安装包和插件在7个国家有20多个镜 ...

  3. git查看一个文件的历史记录

    git log --all -- '*'FILENAME'*' 支持模糊查询 主要用于查找历史上被删除的文件 可以做成git的一个快捷命令 find = "!f(){ git log --a ...

  4. hibernate主键生成策略(转载)

    http://www.cnblogs.com/kakafra/archive/2012/09/16/2687569.html 1.assigned 主键由外部程序负责生成,在 save() 之前必须指 ...

  5. c#访问http接口的"编码"问题

    记一次访问http数据接口的爬坑经历,一般访问一个http接口. 无非就是这么几行代码: HttpWebRequest request = (HttpWebRequest)WebRequest.Cre ...

  6. 从混战到三足鼎立,外卖O2O下一个谁先出局?

    来自第三方数据挖掘和分析机构权威iiMedia Research(艾媒咨询)发布的<2016Q3中国在线餐饮外卖市场专题研究报告>显示,2016Q3中国在线餐饮外卖市场活跃用户分布方面,美 ...

  7. 单元测试之NSNull 检测

    本文主要讲 单元测试之NSNull 检测,在现实开发中,我们最烦的往往就是服务端返回的数据中隐藏着NSNull的数据,一般我们的做法是通过[data isKindOfClass:[NSNull cla ...

  8. Inno setup 安装*.inf文件_示例

    nno setup 调用*.Inf文件的条目区段名称_示例 首先自己编写一个INF文件来供 Inno setup 进行测试: ;复制以下代码到记事本然后另存为123.inf .然后把123.inf文件 ...

  9. PHP之負載均衡下的session共用

    最近忙於開發台灣運動彩券第四版的程式,所以已經很久沒有上來寫東西了,今天隨便寫點東西和大家分享. 首先說一下負載均衡,相信大家都知道負載均衡可以很好地解決網站大流量的問題,負載均衡就是把用戶的請求分發 ...

  10. c# 压缩文件

    递归实现压缩文件夹和子文件夹. using System; using System.Collections.Generic; using System.Linq; using System.Text ...