哈夫曼树
https://www.cnblogs.com/smile...
相关概念
1、叶子结点的权值(weight)是对叶子结点赋予的一个有意义的数值量。
2、设二叉树有n个带权值的叶子结点,从根节点到各个叶子结点的路径长度与相应叶子结点权值的乘积之和叫做二叉树的带权路径长度。
3、给定一组具有确定权值的叶子结点,可以构造出不同的二叉树,将其中带权路径长度最小的二叉树称为哈夫曼树。
哈夫曼算法基本思想
(1) 以权值分别为W1,W2...Wn的n各结点,构成n棵二叉树T1,T2,...Tn并组成森林F={T1,T2,...Tn},其中每棵二叉树 Ti仅有一个权值为 Wi的根结点;
(2) 在F中选取两棵根结点权值最小的树作为左右子树构造一棵新二叉树,并且置新二叉树根结点权值为左右子树上根结点的权值之和(根结点的权值=左右孩子权值之和,叶结点的权值= Wi);
(3) 从F中删除这两棵二叉树,同时将新二叉树加入到F中;
(4) 重复(2)、(3)直到F中只含一棵二叉树为止,这棵二叉树就是Huffman树。
哈夫曼算法的存储结构
考虑到对于有n个叶子结点的哈夫曼树有2n-1个结点,并且进行n-1次合并操作,为了便于选取根节点权值最小的二叉树以及合并操作,设置一个数组haftree[2n-1],保存哈夫曼树中的各个结点的信息,数组元素的结点结构如下图所示:
weight | lchild | rchild | parent |
---|---|---|---|
哈夫曼树的结点结构
其中,weight保存结点权值;
lchild保存该节点的左孩子在数组中的下标;
rchild保存该节点的右孩子在数组中的下标;
parent保存该节点的双亲孩子在数组中的下标。
可以用C++语言中的结构体类型定义上述结点,如下:
/ 哈夫曼树的结点结构
struct element
{
int weight; // 权值域
int lchild, rchild, parent; // 该结点的左、右、双亲结点在数组中的下标
};
哈夫曼编码C++实现
为了判定一个结点是否已经加入哈夫曼树中,可通过parent域的值来确定。初始时parent的值为-1,当某结点加入到树中时,该节点parent域的值为其双亲结点在数组中的下标。
构造哈夫曼树时,首先将n个权值的叶子结点存放到数组haftree的前n个分量中,然后不断将两棵子树合并为一棵子树,并将新子树的根节点顺序存放到数组haftree的前n个分量的后面。
哈夫曼算法用伪代码描述为:
1、数组haftree初始化,所有数组元素的双亲、左右孩子都置为-1;
2、数组haftree的前n个元素的权值置给定权值;
3、进行n-1次合并
3.1 在二叉树集合中选取两个权值最小的根节点,其下标分别为i1,i2;
3.2 将二叉树i1、i2合并为一棵新的二叉树k。
Code
#include
#include //这个头文件是声明一些 “流操作符”的
//比较常用的有:setw(int);//设置显示宽度,left//right//设置左右对齐。 setprecision(int);//设置浮点数的精确度。
using namespace std;
// 哈夫曼树的结点结构
struct element
{
int weight; // 权值域
int lchild, rchild, parent; // 该结点的左、右、双亲结点在数组中的下标
};
// 选取权值最小的两个结点
void selectMin(element a[],int n, int &s1, int &s2)
{
for (int i = 0; i < n; i++)
{
if (a[i].parent == -1)// 初始化s1,s1的双亲为-1
{
s1 = i;
break;
}
}
for (int i = 0; i < n; i++)// s1为权值最小的下标
{
if (a[i].parent == -1 && a[s1].weight > a[i].weight)
s1 = i;
}
for (int j = 0; j < n; j++)
{
if (a[j].parent == -1&&j!=s1)// 初始化s2,s2的双亲为-1
{
s2 = j;
break;
}
}
for (int j = 0; j < n; j++)// s2为另一个权值最小的结点
{
if (a[j].parent == -1 && a[s2].weight > a[j].weight&&j != s1)
s2 = j;
}
}
// 哈夫曼算法
// n个叶子结点的权值保存在数组w中
void HuffmanTree(element huftree[], int w[], int n)
{
for (int i = 0; i < 2*n-1; i++) // 初始化,所有结点均没有双亲和孩子
{
huftree[i].parent = -1;
huftree[i].lchild = -1;
huftree[i].rchild = -1;
}
for (int i = 0; i < n; i++) // 构造只有根节点的n棵二叉树
{
huftree[i].weight = w[i];
}
for (int k = n; k < 2 * n - 1; k++) // n-1次合并
{
int i1, i2;
selectMin(huftree, k, i1, i2); // 查找权值最小的俩个根节点,下标为i1,i2
// 将i1,i2合并,且i1和i2的双亲为k
huftree[i1].parent = k;
huftree[i2].parent = k;
huftree[k].lchild = i1;
huftree[k].rchild = i2;
huftree[k].weight = huftree[i1].weight + huftree[i2].weight;
}
} // 打印哈夫曼树
void print(element hT[],int n)
{
cout << "index weight parent lChild rChild" << endl;
cout << left; // 左对齐输出
for (int i = 0; i < n; ++i)
{
cout << setw(5) << i << " ";
cout << setw(6) << hT[i].weight << " ";
cout << setw(6) << hT[i].parent << " ";
cout << setw(6) << hT[i].lchild << " ";
cout << setw(6) << hT[i].rchild << endl;
}
}
int main()
{
int x[] = { 5,29,7,8,14,23,3,11 }; // 权值集合
element *hufftree=new element[2*8-1]; // 动态创建数组
HuffmanTree(hufftree, x, 8);
print(hufftree,15);
system("pause");
return 0;
}
二叉平衡树(AVL树)
AVL简介
AVL树种的任意节点的左右子树的高度差的绝对值最大为1,其本质是带了平衡功能的二叉搜索树。
二叉搜索树在数据极端情况下会退化成单链表,时间复杂度也会退化成O(n)。而AVL树定义了旋转操作,在平衡因子大于2时,通过旋转来调整树的结构,来重新满足平衡因子小于2,确保在查找、插入和删除在平均和最坏情况下都是O(logn)。
AVL旋转
AVL旋转是AVL树最核心的部分,需要重点掌握。在理解AVL旋转之前先知道以下几个概念:
- AVL树节点的插入总是在叶子节点;
- AVL树在插入节点之前是满足平衡条件的;
- 插入新节点后有可能满足平衡条件也可能不满足;
- 当不满足平衡条件时需要对新的树进行旋转。
旋转之前首先需要找到插入节点向上第一个不平衡的节点(记为A),新插入节点只能在A的的左子树的左子树、左子树的右子树、右子树的左子树、右子树的右子树上,对应四种不同的旋转方式。
如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。这种失去平衡的可以概括为4种姿态:LL(左左),LR(左右),RR(右右)和RL(右左)。下面给出它们的示意图:
上图中的4棵树都是"失去平衡的AVL树",从左往右的情况依次是:LL、LR、RL、RR。除了上面的情况之外,还有其它的失去平衡的AVL树,如下图:
上面的两张图都是为了便于理解,而列举的关于"失去平衡的AVL树"的例子。总的来说,AVL树失去平衡时的情况一定是LL、LR、RL、RR这4种之一,它们都由各自的定义:
(1) LL:LeftLeft,也称为"左左"。插入或删除一个节点后,根节点的左子树的左子树还有非空子节点,导致"根的左子树的高度"比"根的右子树的高度"大2,导致AVL树失去了平衡。
例如,在上面LL情况中,由于"根节点(8)的左子树(4)的左子树(2)还有非空子节点",而"根节点(8)的右子树(12)没有子节点";导致"根节点(8)的左子树(4)高度"比"根节点(8)的右子树(12)"高2。
(2) LR:LeftRight,也称为"左右"。插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,导致"根的左子树的高度"比"根的右子树的高度"大2,导致AVL树失去了平衡。
例如,在上面LR情况中,由于"根节点(8)的左子树(4)的左子树(6)还有非空子节点",而"根节点(8)的右子树(12)没有子节点";导致"根节点(8)的左子树(4)高度"比"根节点(8)的右子树(12)"高2。
(3) RL:RightLeft,称为"右左"。插入或删除一个节点后,根节点的右子树的左子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。
例如,在上面RL情况中,由于"根节点(8)的右子树(12)的左子树(10)还有非空子节点",而"根节点(8)的左子树(4)没有子节点";导致"根节点(8)的右子树(12)高度"比"根节点(8)的左子树(4)"高2。
(4) RR:RightRight,称为"右右"。插入或删除一个节点后,根节点的右子树的右子树还有非空子节点,导致"根的右子树的高度"比"根的左子树的高度"大2,导致AVL树失去了平衡。
例如,在上面RR情况中,由于"根节点(8)的右子树(12)的右子树(14)还有非空子节点",而"根节点(8)的左子树(4)没有子节点";导致"根节点(8)的右子树(12)高度"比"根节点(8)的左子树(4)"高2。
前面说过,如果在AVL树中进行插入或删除节点后,可能导致AVL树失去平衡。AVL失去平衡之后,可以通过旋转使其恢复平衡,下面分别介绍"LL(左左),LR(左右),RR(右右)和RL(右左)"这4种情况对应的旋转方法。
3.1 LL旋转
/*
LL
在左左旋转中,一共涉及到三代节点,我们把爷爷节点命名为K2,K2的左儿子命名为K1。
问题出现的原因是K1的左儿子增加了一个节点导致平衡树失衡
解决思路:
让K1成为爷爷节点,K2成为K1的右儿子,并且将K1的右儿子接为K2的左儿子,然后返回爷爷节点K1取代原来K2的位置
*/
template
AVLTreeNode* AVLTree::LL_Rotation(AVLTreeNode* k2){
AVLTreeNode* k1;
k1 = k2->Left;
k2->Left = k1->Right;
k1->Right = k2;
k2->height = max( height(k2->Left), height(k2->Right)) + 1;
k1->height = max( height(k1->Left), k2->height) + 1;
return k1;
}
3.2RR旋转
/*
RR
在右右旋转中,一共涉及到三代节点,我们把爷爷节点命名为K1,K1的右儿子命名为K2。
问题出现的原因是K2的右儿子增加了一个节点导致平衡树失衡
解决思路:
让K2成为爷爷节点,K1成为K2的左儿子,并且将K2的左儿子接为K1的右儿子,然后返回爷爷节点K2取代原来K1的位置
*/
template
AVLTreeNode* AVLTree::RR_Rotation(AVLTreeNode* k1){
AVLTreeNode* k2;
k2 = k1->Right;
k1->Right = k2->Left;
k2->Left = k1;
k1->height = max( height(k1->Left), height(k1->Right)) + 1;
k2->height = max( height(k2->Right), k1->height) + 1;
return k2;
}
3.3LR旋转
/*
LR
在左右旋转中,一共涉及到四代节点,我们把做根本的节点成为K3(曾爷爷节点),K3的左儿子称为K1(爷爷节点),K1的右儿子称为K2
问题出现的原因时K2的右儿子增加了一个节点之后导致树的失衡
解决思路:
因为涉及到四代节点,所以需要两次旋转,
首先对K1,K2进行一次右右旋转 =》 K2成为爷爷节点(即K3的左儿子),k2原本的左儿子称为K1的右儿子,K1成为K2的左儿子
接下来对K2,K3进行一次左左旋转 =》K2称为曾爷爷节点,K2原本的右儿子成为K3的左儿子,K3成为K2的右儿子
*/
template
AVLTreeNode* AVLTree::LR_Rotation(AVLTreeNode* k3){
k3->Left = RR_Rotation(k3->Left);
return LL_Rotation(k3);
}
3.4RL旋转
/*
RL
在右左旋转中,一共涉及到四代节点,我们把做根本的节点成为K1(曾爷爷节点),K1的右儿子称为K3(爷爷节点),K3的左儿子称为K2
问题出现的原因时K2的左儿子增加了一个节点之后导致树的失衡
解决思路:
因为涉及到四代节点,所以需要两次旋转,
首先对K2,K3进行一次左左旋转 =》 K2成为爷爷节点(即K1的右儿子),k2原本的右儿子称为K3的左儿子,K3成为K2的右儿子
接下来对K1,K2进行一次右右旋转 =》K2称为曾爷爷节点,K2原本的左儿子成为K1的右儿子,K1成为K2的左儿子
*/
template
AVLTreeNode* AVLTree::RL_Rotation(AVLTreeNode* k1){
k1->Right = LL_Rotation(k1->Right);
return RR_Rotation(k1);
}
4.插入节点
template
AVLTreeNode* AVLTree::add(AVLTreeNode* &tree, T data){
if (tree == NULL) {
tree = new AVLTreeNode(data, NULL, NULL);
}
else if (data < tree->data){
//将新加入的节点插入左子树
tree->Left = add(tree->Left, data);
//检查加入新的结点之后树是否失去平衡
if (height(tree->Left) - height(tree->Right) == 2)
{
if (data < tree->Left->data)
tree = LL_Rotation(tree);//左左,新加入之后左儿子的左儿子深了
else
tree = LR_Rotation(tree);//左右,新加入之后左儿子的右儿子深了
}
}
//将新加入的节点插入右子树
else if (data > tree->data) {
tree->Right = add(tree->Right, data);
//检查加入新的结点之后树是否失去平衡
if (height(tree->Right) - height(tree->Left) == 2)
{
if (data > tree->Right->data)
tree = RR_Rotation(tree);//右右,新加入之后右儿子的右儿子深了
else
tree = RL_Rotation(tree);//右左,新加入之后右儿子的左儿子深了
}
}
else //该节点已经在树中
{
cout << "该节点已经存在树中" << endl;
}
//更新更前当前节点的高度
tree->height = max( height(tree->Left), height(tree->Right)) + 1;
return tree;
}
template
void AVLTree::add(T data){
add(Root, data);
}
Code
#include
#include
using namespace std;
template
struct AVLTreeNode{
T data;
int height;
AVLTreeNode* Left;
AVLTreeNode* Right;
AVLTreeNode(T v,AVLTreeNode* l,AVLTreeNode* r):data(v),height(0),Left(l),Right(r){}
};
/*
AVL树的定义
为了保护类内数据,仿照网络实例把函数写成了内接口和外接口的形式。还有模板类。
感觉代码有点繁杂,写完之后调式的时候感觉不太顺手,以后写程序要注意内接口和外接口的模式
*/
template
class AVLTree{
private:
AVLTreeNode* Root;
public:
AVLTree():Root(NULL){}
void add(T data);
int height();
int max(int a, int b);
private:
AVLTreeNode* add(AVLTreeNode* &tree, T data);
int height(AVLTreeNode* tree);
AVLTreeNode* LL_Rotation(AVLTreeNode* k2);
AVLTreeNode* RR_Rotation(AVLTreeNode* k1);
AVLTreeNode* LR_Rotation(AVLTreeNode* k3);
AVLTreeNode* RL_Rotation(AVLTreeNode* k1);
};
/*
高度
作用:获取树的高度
*/
template
int AVLTree::height(AVLTreeNode* tree)
{
if (tree != NULL)
return tree->height;
return 0;
}
template
int AVLTree::height() {
return height(Root);
}
/* 模板类改造比较两个值的大小*/
template
int AVLTree::max(int a, int b) {
return a>b ? a : b;
}
/*
LL
在左左旋转中,一共涉及到三代节点,我们把爷爷节点命名为K2,K2的左儿子命名为K1。
问题出现的原因是K1的左儿子增加了一个节点导致平衡树失衡
解决思路:
让K1成为爷爷节点,K2成为K1的右儿子,并且将K1的右儿子接为K2的左儿子,然后返回爷爷节点K1取代原来K2的位置
*/
template
AVLTreeNode* AVLTree::LL_Rotation(AVLTreeNode* k2){
AVLTreeNode* k1;
k1 = k2->Left;
k2->Left = k1->Right;
k1->Right = k2;
k2->height = max( height(k2->Left), height(k2->Right)) + 1;
k1->height = max( height(k1->Left), k2->height) + 1;
return k1;
}
/*
RR
在右右旋转中,一共涉及到三代节点,我们把爷爷节点命名为K1,K1的右儿子命名为K2。
问题出现的原因是K2的右儿子增加了一个节点导致平衡树失衡
解决思路:
让K2成为爷爷节点,K1成为K2的左儿子,并且将K2的左儿子接为K1的右儿子,然后返回爷爷节点K2取代原来K1的位置
*/
template
AVLTreeNode* AVLTree::RR_Rotation(AVLTreeNode* k1){
AVLTreeNode* k2;
k2 = k1->Right;
k1->Right = k2->Left;
k2->Left = k1;
k1->height = max( height(k1->Left), height(k1->Right)) + 1;
k2->height = max( height(k2->Right), k1->height) + 1;
return k2;
}
/*
LR
在左右旋转中,一共涉及到四代节点,我们把做根本的节点成为K3(曾爷爷节点),K3的左儿子称为K1(爷爷节点),K1的右儿子称为K2
问题出现的原因时K2的右儿子增加了一个节点之后导致树的失衡
解决思路:
因为涉及到四代节点,所以需要两次旋转,
首先对K1,K2进行一次右右旋转 =》 K2成为爷爷节点(即K3的左儿子),k2原本的左儿子称为K1的右儿子,K1成为K2的左儿子
接下来对K2,K3进行一次左左旋转 =》K2称为曾爷爷节点,K2原本的右儿子成为K3的左儿子,K3成为K2的右儿子
*/
template
AVLTreeNode* AVLTree::LR_Rotation(AVLTreeNode* k3){
k3->Left = RR_Rotation(k3->Left);
return LL_Rotation(k3);
}
/*
RL
在右左旋转中,一共涉及到四代节点,我们把做根本的节点成为K1(曾爷爷节点),K1的右儿子称为K3(爷爷节点),K3的左儿子称为K2
问题出现的原因时K2的左儿子增加了一个节点之后导致树的失衡
解决思路:
因为涉及到四代节点,所以需要两次旋转,
首先对K2,K3进行一次左左旋转 =》 K2成为爷爷节点(即K1的右儿子),k2原本的右儿子称为K3的左儿子,K3成为K2的右儿子
接下来对K1,K2进行一次右右旋转 =》K2称为曾爷爷节点,K2原本的左儿子成为K1的右儿子,K1成为K2的左儿子
*/
template
AVLTreeNode* AVLTree::RL_Rotation(AVLTreeNode* k1){
k1->Right = LL_Rotation(k1->Right);
return RR_Rotation(k1);
}
template
AVLTreeNode* AVLTree::add(AVLTreeNode* &tree, T data){
if (tree == NULL) {
tree = new AVLTreeNode(data, NULL, NULL);
}
else if (data < tree->data){
//将新加入的节点插入左子树
tree->Left = add(tree->Left, data);
//检查加入新的结点之后树是否失去平衡
if (height(tree->Left) - height(tree->Right) == 2)
{
if (data < tree->Left->data)
tree = LL_Rotation(tree);//左左,新加入之后左儿子的左儿子深了
else
tree = LR_Rotation(tree);//左右,新加入之后左儿子的右儿子深了
}
}
//将新加入的节点插入右子树
else if (data > tree->data) {
tree->Right = add(tree->Right, data);
//检查加入新的结点之后树是否失去平衡
if (height(tree->Right) - height(tree->Left) == 2)
{
if (data > tree->Right->data)
tree = RR_Rotation(tree);//右右,新加入之后右儿子的右儿子深了
else
tree = RL_Rotation(tree);//右左,新加入之后右儿子的左儿子深了
}
}
else //该节点已经在树中
{
cout << "该节点已经存在树中" << endl;
}
//更新更前当前节点的高度
tree->height = max( height(tree->Left), height(tree->Right)) + 1;
return tree;
}
template
void AVLTree::add(T data){
add(Root, data);
}
int main(){
int num;
AVLTree* tree=new AVLTree();
cin>>num;
for(int i=0;i>x;
tree->add(x);
}
cout<<"高度为:"<height()<
B树和B+树
https://blog.csdn.net/qq_2594...
https://blog.csdn.net/z_ryan/...
B树
如果前面的2-3树与2-3-4树理解了,B树也就理解了,因为2-3树就是3阶的B树,2-3-4树就是4阶的B树。所以,对于B树的性质,根据2-3-4树都可以推导出来了,即,
一颗m阶的B树(B-tree) 定义如下:
(1)每个节点最多有 m-1 个key;
(2)根节点至少有1个key;
(3)非根节点至少有 Math.ceil(m/2)-1 个key;
(4)每个节点中的key都按照从小到大的顺序排列,每个key的左子树中的所有key都小于它,而右子树中的所有key都大于它;
(5)所有叶子节点都位于同一层,即根节点到每个叶子节点的长度都相同。
在前面的章节我就已经说过,出现多路查找树的原因,是因为多路查找树的数据结构,用在内存读取外存的场景下,可以减少磁盘的IO次数,因为在高阶的情况下,树不用很高就可以标识很大的数据量了,那这个怎么算的呢?
打个比方,以2-3树为例,树高为3的时候,一棵2-3树可以保存2+3x2+3x2x2=20个key,若当B树的阶数达到1001阶,即一个节点可以放1000个key,然后树高还是3,即 1000+1000x1001+1000x1001x1000 ,零头不算了,即至少可以放10个亿的key,此时我们只要让根节点读取到内存中,把子节点及子孙节点持久化到硬盘中,那么在这棵树上,寻找某一个key至多需要2次硬盘的读取即可。
而对于B树节点的插入,可以类比2-3-4树,即,若节点插入节点的key还未“丰满”,则直接插入,若节点插入节点的key已“丰满”,则插入节点之后分裂,再以分裂之后的父节点看作向上层插入的节点调整,直至满足该 m 阶的B树。如下,为5阶B树插入节点的动态图,
对于B树节点的删除,也一样类比2-3-4树,如下,
(1)若删除非叶子节点, 找后继节点替换之,将问题转化为删除叶子节点;
(2)若删除叶子节点,且叶子节点的key数大于定义中的最小值(根节点至少有1个key,非根节点至少有 Math.ceil(m/2)-1 个key),则直接删除即可,无需调整,
(3)若删除叶子节点,且叶子节点的key数刚好满足定义中的最小值,即刚好“脱贫”,则将节点删除,此时树肯定需要调整,即:
a.若删除节点的相邻兄弟节点的key数“富裕”(节点的key大于定义中的最小值),则父节点的1个key下移与待删除的节点合并,相邻兄弟节点的1个key上移与父节点合并,完成调整;
b.若删除节点的相邻兄弟节点的key数刚好“脱贫”(节点的key刚好满足定义的最小值),则父节点的1个key下移与待删除的节点及相邻兄弟节点,三者进行合并成一个节点,若下移1个key后的父节点的key数刚好“脱贫”或“富裕”,则调整完成,反之,即此时父节点已经陷入“贫穷”,则将父节点看作当前待删除的节点,重复a,b的判断。
特征
一个m阶的B树具有如下几个特征:B树中所有结点的孩子结点最大值称为B树的阶,通常用m表示。一个结点有k个孩子时,必有k-1个关键字才能将子树中所有关键字划分为k个子集。
1.根结点至少有两个子女。
2.每个中间节点都包含k-1个元素和k个孩子,其中 ceil(m/2) ≤ k ≤ m
3.每一个叶子节点都包含k-1个元素,其中 ceil(m/2) ≤ k ≤ m
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域划分
6.每个结点的结构为:(n,A0,K1,A1,K2,A2,… ,Kn,An)其中,Ki(1≤i≤n)为关键字,且Ki
Ai(0≤i≤n)为指向子树根结点的指针。且Ai所指子树所有结点中的关键字均小于Ki+1。
n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。
示例:三阶B树(实际中节点中元素很多)
查询
以上图为例:若查询的数值为5:
第一次磁盘IO:在内存中定位(与17、35比较),比17小,左子树;
第二次磁盘IO:在内存中定位(与8、12比较),比8小,左子树;
第三次磁盘IO:在内存中定位(与3、5比较),找到5,终止。
整个过程中,我们可以看出:比较的次数并不比二叉查找树少,尤其适当某一节点中的数据很多时,但是磁盘IO的次数却是大大减少。比较是在内存中进行的,相比于磁盘IO的速度,比较的耗时几乎可以忽略。所以当树的高度足够低的话,就可以极大的提高效率。相比之下,节点中的元素多点也没关系,仅仅是多了几次内存交互而已,只要不超过磁盘页的大小即可。
插入
对高度为k的m阶B树,新结点一般是插在叶子层。通过检索可以确定关键码应插入的结点位置。然后分两种情况讨论:
1、 若该结点中关键码个数小于m-1,则直接插入即可。
2、 若该结点中关键码个数等于m-1,则将引起结点的分裂。以中间关键码为界将结点一分为二,产生一个新结点,并把中间关键码插入到父结点(k-1层)中
重复上述工作,最坏情况一直分裂到根结点,建立一个新的根结点,整个B树增加一层。
例如:在下面的B树中插入key:4
第一步:检索key插入的节点位置如上图所示,在3,5之间;
第二步:判断节点中的关键码个数:
节点3,5已经是两元素节点,无法再增加。父亲节点 2, 6 也是两元素节点,也无法再增加。根节点9是单元素节点,可以升级为两元素节点。;
第三步:结点分裂:
拆分节点3,5与节点2,6,让根节点9升级为两元素节点4,9。节点6独立为根节点的第二个孩子。
最终结果如下图:虽然插入比较麻烦,但是这也能确保B树是一个自平衡的树
删除
B树中关键字的删除比插入更复杂,在这里,只介绍其中的一种方法:
在B树上删除关键字k的过程分两步完成:
(1)找出该关键字所在的结点。然后根据 k所在结点是否为叶子结点有不同的处理方法。
(2)若该结点为非叶结点,且被删关键字为该结点中第i个关键字key[i],则可从指针son[i]所指的子树中
找出最小关键字Y,代替key[i]的位置,然后在叶结点中删去Y。
因此,把在非叶结点删除关键字k的问题就变成了删除叶子结点中的关键字的问题了。
在B-树叶结点上删除一个关键字的方法:
首先将要删除的关键字 k直接从该叶子结点中删除。然后根据不同情况分别作相应的处理,共有三种可能情况:
(1)如果被删关键字所在结点的原关键字个数n>=ceil(m/2),说明删去该关键字后该结点仍满足B树的定义。
这种情况最为简单,只需从该结点中直接删去关键字即可。(2)如果被删关键字所在结点的关键字个数n等于ceil(m/2)-1,说明删去该关键字后该结点将不满足B树的定义,
需要调整。调整过程为:
如果其左右兄弟结点中有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目大于
ceil(m/2)-1。则可将右(左)兄弟结点中最小(大)关键字上移至双亲结点。而将双亲结点中小(大)于该上
移关键字的关键字下移至被删关键字所在结点中。
如果左右兄弟结点中没有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目均等于
ceil(m/2)-1。这种情况比较复杂。需把要删除关键字的结点与其左(或右)兄弟结点以及双亲结点中分割二者
的关键字合并成一个结点,即在删除关键字后,该结点中剩余的关键字加指针,加上双亲结点中的关键字Ki一起,
合并到Ai(是双亲结点指向该删除关键字结点的左(右)兄弟结点的指针)所指的兄弟结点中去。如果因此使双亲
结点中关键字个数小于ceil(m/2)-1,则对此双亲结点做同样处理。以致于可能直到对根结点做这样的处理而使
整个树减少一层。
总之,设所删关键字为非终端结点中的Ki,则可以指针Ai所指子树中的最小关键字Y代替Ki,然后在相应结点中删除Y。对任意关键字的删除都可以转化为对最下层关键字的删除。
下面举一个简单的例子:删除元素11.
第一步:判断该元素是否在叶子结点上。
该元素在叶子节点上,可以直接删去,但是删除之后,中间节点12只有一个孩子,不符合B树的定义:每个中间节点都包含k个孩子,(其中 ceil(m/2) <= k <= m)所以需要调整;
第二步:判断其左右兄弟结点中有“多余”的关键字,即:原关键字个数n>=ceil(m/2) - 1;
显然结点11的右兄弟节点中有多余的关键字。那么可将右兄弟结点中最小关键字上移至双亲结点。而将双亲结点中小于该上移关键字的关键字下移至被删关键字所在结点中即可
注意
①、B树主要用于文件系统以及部分数据库索引,例如: MongoDB。而大部分关系数据库则使用B+树做索引,例如:mysql数据库;
②、从查找效率考虑一般要求B树的阶数m >= 3;
③、B-树上算法的执行时间主要由读、写磁盘的次数来决定,故一次I/O操作应读写尽可能多的信息。因此B-树的结点规模一般以一个磁盘页为单位。一个结点包含的关键字及其孩子个数取决于磁盘页的大小。
B+树
虽然B树这种数据结构,应用在内外存交互,可以极大的减少磁盘的IO次数,但还是有些小瑕疵,如下5阶的B树图,若我需要读取key为“66”与“73”的数据,则此时从根节点“50”开始,“66”大于“50”,找右孩子,即到“60 70 120”的节点,再锁定到“64 66”的节点,找到key为“66”的数据,然后读“73”的数据,再重新从根开始往下寻找key为“73”的数据,如果需要查询的数据量一多,性能就很糟糕。还有一点,就是B树的每个节点都包含key及其value数据,这样的话,我每次读取叶子节点的数据时,在经过路径上的非叶子节点也会被读出,但实际上这部分数据我是不需要的,这样又占用了没有必要的内存空间。
所以,B+树在B树的基础上做了优化,它与B树的差异在于:
(1)有 k 个子节点的节点必然有 k 个key;
(2)非叶子节点仅具有索引作用,跟记录有关的信息均存放在叶子节点中。
(3)树的所有叶子节点构成一个有序链表,可以按照key排序的次序遍历全部记录。
即,B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。
B+树的优点在于:
1.由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。
数据存放的更加紧密,具有更好的空间局部性。
因此访问叶子节点上关联的数据也具有更好的缓存命中率。
2.B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。
而且由于数据顺序排列并且相连,所以便于区间查找和搜索。
而B树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。但是B树也有优点,其优点在于:
由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
特征
一个m阶的B+树具有如下几个特征:
1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据
都保存在叶子节点。2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小
自小而大顺序链接。3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
下面是一棵3阶的B+树:
B+树通常有两个指针,一个指向根结点,另一个指向关键字最小的叶子结点。因些,对于B+树进行查找两种运算:一种是从最小关键字起顺序查找,另一种是从根结点开始,进行随机查找。
查找
B+树的优势在于查找效率上,下面我们做一具体说明:
首先,B+树的查找和B树一样,类似于二叉查找树。起始于根节点,自顶向下遍历树,选择其分离值在要查找值的任意一边的子指针。在节点内部典型的使用是二分查找来确定这个位置。
(1)、不同的是,B+树中间节点没有卫星数据(索引元素所指向的数据记录),只有索引,而B树每个结点中的每个关键字都有卫星数据;这就意味着同样的大小的磁盘页可以容纳更多节点元素,在相同的数据量下,B+树更加“矮胖”,IO操作更少
B树的卫星数据:
fcnlhbg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
B+树的卫星数据:
插入
B+树的插入与B树的插入过程类似。不同的是B+树在叶结点上进行,如果叶结点中的关键码个数超过m,就必须分裂成关键码数目大致相同的两个结点,并保证上层结点中有这两个结点的最大关键码。
删除
B+树中的关键码在叶结点层删除后,其在上层的复本可以保留,作为一个”分解关键码”存在,如果因为删除而造成结点中关键码数小于ceil(m/2),其处理过程与B-树的处理一样。在此,我就不多做介绍了。
B+树 Code
不一定能跑,仅供参考
#ifndef BPLUS_NODE
#define BPLUS_NODE
#define NULL 0
enum NODE_TYPE{INTERNAL, LEAF}; // 结点类型:内结点、叶子结点
enum SIBLING_DIRECTION{LEFT, RIGHT}; // 兄弟结点方向:左兄弟结点、右兄弟结点
typedef float KeyType; // 键类型
typedef int DataType; // 值类型
const int ORDER = 7; // B+树的阶(非根内结点的最小子树个数)
const int MINNUM_KEY = ORDER-1; // 最小键值个数
const int MAXNUM_KEY = 2*ORDER-1; // 最大键值个数
const int MINNUM_CHILD = MINNUM_KEY+1; // 最小子树个数
const int MAXNUM_CHILD = MAXNUM_KEY+1; // 最大子树个数
const int MINNUM_LEAF = MINNUM_KEY; // 最小叶子结点键值个数
const int MAXNUM_LEAF = MAXNUM_KEY; // 最大叶子结点键值个数
// 结点基类
class CNode{
public:
CNode();
virtual ~CNode();
NODE_TYPE getType() const {return m_Type;}
void setType(NODE_TYPE type){m_Type = type;}
int getKeyNum() const {return m_KeyNum;}
void setKeyNum(int n){m_KeyNum = n;}
KeyType getKeyValue(int i) const {return m_KeyValues[i];}
void setKeyValue(int i, KeyType key){m_KeyValues[i] = key;}
int getKeyIndex(KeyType key)const; // 找到键值在结点中存储的下标
// 纯虚函数,定义接口
virtual void removeKey(int keyIndex, int childIndex)=0; // 从结点中移除键值
virtual void split(CNode* parentNode, int childIndex)=0; // 分裂结点
virtual void mergeChild(CNode* parentNode, CNode* childNode, int keyIndex)=0; // 合并结点
virtual void clear()=0; // 清空结点,同时会清空结点所包含的子树结点
virtual void borrowFrom(CNode* destNode, CNode* parentNode, int keyIndex, SIBLING_DIRECTION d)=0; // 从兄弟结点中借一个键值
virtual int getChildIndex(KeyType key, int keyIndex)const=0; // 根据键值获取孩子结点指针下标
protected:
NODE_TYPE m_Type;
int m_KeyNum;
KeyType m_KeyValues[MAXNUM_KEY];
};
// 内结点
class CInternalNode : public CNode{
public:
CInternalNode();
virtual ~CInternalNode();
CNode* getChild(int i) const {return m_Childs[i];}
void setChild(int i, CNode* child){m_Childs[i] = child;}
void insert(int keyIndex, int childIndex, KeyType key, CNode* childNode);
virtual void split(CNode* parentNode, int childIndex);
virtual void mergeChild(CNode* parentNode, CNode* childNode, int keyIndex);
virtual void removeKey(int keyIndex, int childIndex);
virtual void clear();
virtual void borrowFrom(CNode* destNode, CNode* parentNode, int keyIndex, SIBLING_DIRECTION d);
virtual int getChildIndex(KeyType key, int keyIndex)const;
private:
CNode* m_Childs[MAXNUM_CHILD];
};
// 叶子结点
class CLeafNode : public CNode{
public:
CLeafNode();
virtual ~CLeafNode();
CLeafNode* getLeftSibling() const {return m_LeftSibling;}
void setLeftSibling(CLeafNode* node){m_LeftSibling = node;}
CLeafNode* getRightSibling() const {return m_RightSibling;}
void setRightSibling(CLeafNode* node){m_RightSibling = node;}
DataType getData(int i) const {return m_Datas[i];}
void setData(int i, const DataType& data){m_Datas[i] = data;}
void insert(KeyType key, const DataType& data);
virtual void split(CNode* parentNode, int childIndex);
virtual void mergeChild(CNode* parentNode, CNode* childNode, int keyIndex);
virtual void removeKey(int keyIndex, int childIndex);
virtual void clear();
virtual void borrowFrom(CNode* destNode, CNode* parentNode, int keyIndex, SIBLING_DIRECTION d);
virtual int getChildIndex(KeyType key, int keyIndex)const;
private:
CLeafNode* m_LeftSibling;
CLeafNode* m_RightSibling;
DataType m_Datas[MAXNUM_LEAF];
};
#endif
#include "BPlus_node.h"
// CNode
CNode::CNode(){
setType(LEAF);
setKeyNum(0);
}
CNode::~CNode(){
setKeyNum(0);
}
int CNode::getKeyIndex(KeyType key)const
{
int left = 0;
int right = getKeyNum()-1;
int current;
while(left!=right)
{
current = (left+right)/2;
KeyType currentKey = getKeyValue(current);
if (key>currentKey)
{
left = current+1;
}
else
{
right = current;
}
}
return left;
}
// CInternalNode
CInternalNode::CInternalNode():CNode(){
setType(INTERNAL);
}
CInternalNode::~CInternalNode(){
}
void CInternalNode::clear()
{
for (int i=0; i<=m_KeyNum; ++i)
{
m_Childs[i]->clear();
delete m_Childs[i];
m_Childs[i] = NULL;
}
}
void CInternalNode::split(CNode* parentNode, int childIndex)
{
CInternalNode* newNode = new CInternalNode();//分裂后的右节点
newNode->setKeyNum(MINNUM_KEY);
int i;
for (i=0; isetKeyValue(i, m_KeyValues[i+MINNUM_CHILD]);
}
for (i=0; isetChild(i, m_Childs[i+MINNUM_CHILD]);
}
setKeyNum(MINNUM_KEY); //更新左子树的关键字个数
((CInternalNode*)parentNode)->insert(childIndex, childIndex+1, m_KeyValues[MINNUM_KEY], newNode);
}
void CInternalNode::insert(int keyIndex, int childIndex, KeyType key, CNode* childNode){
int i;
for (i=getKeyNum(); i>keyIndex; --i)//将父节点中的childIndex后的所有关键字的值和子树指针向后移一位
{
setChild(i+1,m_Childs[i]);
setKeyValue(i,m_KeyValues[i-1]);
}
if (i==childIndex)
{
setChild(i+1, m_Childs[i]);
}
setChild(childIndex, childNode);
setKeyValue(keyIndex, key);
setKeyNum(m_KeyNum+1);
}
void CInternalNode::mergeChild(CNode* parentNode, CNode* childNode, int keyIndex)
{
// 合并数据
insert(MINNUM_KEY, MINNUM_KEY+1, parentNode->getKeyValue(keyIndex), ((CInternalNode*)childNode)->getChild(0));
int i;
for (i=1; i<=childNode->getKeyNum(); ++i)
{
insert(MINNUM_KEY+i, MINNUM_KEY+i+1, childNode->getKeyValue(i-1), ((CInternalNode*)childNode)->getChild(i));
}
//父节点删除index的key
parentNode->removeKey(keyIndex, keyIndex+1);
delete ((CInternalNode*)parentNode)->getChild(keyIndex+1);
}
void CInternalNode::removeKey(int keyIndex, int childIndex)
{
for (int i=0; igetKeyValue(keyIndex), ((CInternalNode*)siblingNode)->getChild(siblingNode->getKeyNum()));
parentNode->setKeyValue(keyIndex, siblingNode->getKeyValue(siblingNode->getKeyNum()-1));
siblingNode->removeKey(siblingNode->getKeyNum()-1, siblingNode->getKeyNum());
}
break;
case RIGHT: // 从右兄弟结点借
{
insert(getKeyNum(), getKeyNum()+1, parentNode->getKeyValue(keyIndex), ((CInternalNode*)siblingNode)->getChild(0));
parentNode->setKeyValue(keyIndex, siblingNode->getKeyValue(0));
siblingNode->removeKey(0, 0);
}
break;
default:
break;
}
}
int CInternalNode::getChildIndex(KeyType key, int keyIndex)const
{
if (key==getKeyValue(keyIndex))
{
return keyIndex+1;
}
else
{
return keyIndex;
}
}
// CLeafNode
CLeafNode::CLeafNode():CNode(){
setType(LEAF);
setLeftSibling(NULL);
setRightSibling(NULL);
}
CLeafNode::~CLeafNode(){
}
void CLeafNode::clear()
{
for (int i=0; i=1 && m_KeyValues[i-1]>key; --i)
{
setKeyValue(i, m_KeyValues[i-1]);
setData(i, m_Datas[i-1]);
}
setKeyValue(i, key);
setData(i, data);
setKeyNum(m_KeyNum+1);
}
void CLeafNode::split(CNode* parentNode, int childIndex)
{
CLeafNode* newNode = new CLeafNode();//分裂后的右节点
setKeyNum(MINNUM_LEAF);
newNode->setKeyNum(MINNUM_LEAF+1);
newNode->setRightSibling(getRightSibling());
setRightSibling(newNode);
newNode->setLeftSibling(this);
int i;
for (i=0; isetKeyValue(i, m_KeyValues[i+MINNUM_LEAF]);
}
for (i=0; isetData(i, m_Datas[i+MINNUM_LEAF]);
}
((CInternalNode*)parentNode)->insert(childIndex, childIndex+1, m_KeyValues[MINNUM_LEAF], newNode);
}
void CLeafNode::mergeChild(CNode* parentNode, CNode* childNode, int keyIndex)
{
// 合并数据
for (int i=0; igetKeyNum(); ++i)
{
insert(childNode->getKeyValue(i), ((CLeafNode*)childNode)->getData(i));
}
setRightSibling(((CLeafNode*)childNode)->getRightSibling());
//父节点删除index的key,
parentNode->removeKey(keyIndex, keyIndex+1);
}
void CLeafNode::removeKey(int keyIndex, int childIndex)
{
for (int i=keyIndex; igetKeyValue(siblingNode->getKeyNum()-1), ((CLeafNode*)siblingNode)->getData(siblingNode->getKeyNum()-1));
siblingNode->removeKey(siblingNode->getKeyNum()-1, siblingNode->getKeyNum()-1);
parentNode->setKeyValue(keyIndex, getKeyValue(0));
}
break;
case RIGHT: // 从右兄弟结点借
{
insert(siblingNode->getKeyValue(0), ((CLeafNode*)siblingNode)->getData(0));
siblingNode->removeKey(0, 0);
parentNode->setKeyValue(keyIndex, siblingNode->getKeyValue(0));
}
break;
default:
break;
}
}
int CLeafNode::getChildIndex(KeyType key, int keyIndex)const
{
return keyIndex;
}
#ifndef BPLUS_TREE_H
#define BPLUS_TREE_H
#include "BPlus_node.h"
#include
using namespace std;
enum COMPARE_OPERATOR{LT, LE, EQ, BE, BT, BETWEEN}; // 比较操作符:<、<=、=、>=、>、<>
const int INVALID_INDEX = -1;
struct SelectResult
{
int keyIndex;
CLeafNode* targetNode;
};
class CBPlusTree{
public:
CBPlusTree();
~CBPlusTree();
bool insert(KeyType key, const DataType& data);
bool remove(KeyType key);
bool update(KeyType oldKey, KeyType newKey);
// 定值查询,compareOperator可以是LT(<)、LE(<=)、EQ(=)、BE(>=)、BT(>)
vector select(KeyType compareKey, int compareOpeartor);
// 范围查询,BETWEEN
vector select(KeyType smallKey, KeyType largeKey);
bool search(KeyType key); // 查找是否存在
void clear(); // 清空
void print()const; // 打印树关键字
void printData()const; // 打印数据
private:
void recursive_insert(CNode* parentNode, KeyType key, const DataType& data);
void recursive_remove(CNode* parentNode, KeyType key);
void printInConcavo(CNode *pNode, int count)const;
bool recursive_search(CNode *pNode, KeyType key)const;
void changeKey(CNode *pNode, KeyType oldKey, KeyType newKey);
void search(KeyType key, SelectResult& result);
void recursive_search(CNode* pNode, KeyType key, SelectResult& result);
void remove(KeyType key, DataType& dataValue);
void recursive_remove(CNode* parentNode, KeyType key, DataType& dataValue);
private:
CNode* m_Root;
CLeafNode* m_DataHead;
KeyType m_MaxKey; // B+树中的最大键
};
#endif
#include "BPlus_tree.h"
#include
#include
using namespace std;
CBPlusTree::CBPlusTree(){
m_Root = NULL;
m_DataHead = NULL;
}
CBPlusTree::~CBPlusTree(){
clear();
}
bool CBPlusTree::insert(KeyType key, const DataType& data){
// 是否已经存在
if (search(key))
{
return false;
}
// 找到可以插入的叶子结点,否则创建新的叶子结点
if(m_Root==NULL)
{
m_Root = new CLeafNode();
m_DataHead = (CLeafNode*)m_Root;
m_MaxKey = key;
}
if (m_Root->getKeyNum()>=MAXNUM_KEY) // 根结点已满,分裂
{
CInternalNode* newNode = new CInternalNode(); //创建新的根节点
newNode->setChild(0, m_Root);
m_Root->split(newNode, 0); // 叶子结点分裂
m_Root = newNode; //更新根节点指针
}
if (key>m_MaxKey) // 更新最大键值
{
m_MaxKey = key;
}
recursive_insert(m_Root, key, data);
return true;
}
void CBPlusTree::recursive_insert(CNode* parentNode, KeyType key, const DataType& data)
{
if (parentNode->getType()==LEAF) // 叶子结点,直接插入
{
((CLeafNode*)parentNode)->insert(key, data);
}
else
{
// 找到子结点
int keyIndex = parentNode->getKeyIndex(key);
int childIndex= parentNode->getChildIndex(key, keyIndex); // 孩子结点指针索引
CNode* childNode = ((CInternalNode*)parentNode)->getChild(childIndex);
if (childNode->getKeyNum()>=MAXNUM_LEAF) // 子结点已满,需进行分裂
{
childNode->split(parentNode, childIndex);
if (parentNode->getKeyValue(childIndex)<=key) // 确定目标子结点
{
childNode = ((CInternalNode*)parentNode)->getChild(childIndex+1);
}
}
recursive_insert(childNode, key, data);
}
}
void CBPlusTree::clear()
{
if (m_Root!=NULL)
{
m_Root->clear();
delete m_Root;
m_Root = NULL;
m_DataHead = NULL;
}
}
bool CBPlusTree::search(KeyType key)
{
return recursive_search(m_Root, key);
}
bool CBPlusTree::recursive_search(CNode *pNode, KeyType key)const
{
if (pNode==NULL) //检测节点指针是否为空,或该节点是否为叶子节点
{
return false;
}
else
{
int keyIndex = pNode->getKeyIndex(key);
int childIndex = pNode->getChildIndex(key, keyIndex); // 孩子结点指针索引
if (keyIndexgetKeyNum() && key==pNode->getKeyValue(keyIndex))
{
return true;
}
else
{
if (pNode->getType()==LEAF) //检查该节点是否为叶子节点
{
return false;
}
else
{
return recursive_search(((CInternalNode*)pNode)->getChild(childIndex), key);
}
}
}
}
void CBPlusTree::print()const
{
printInConcavo(m_Root, 10);
}
void CBPlusTree::printInConcavo(CNode *pNode, int count) const{
if (pNode!=NULL)
{
int i, j;
for (i=0; igetKeyNum(); ++i)
{
if (pNode->getType()!=LEAF)
{
printInConcavo(((CInternalNode*)pNode)->getChild(i), count-2);
}
for (j=count; j>=0; --j)
{
cout<<"-";
}
cout<getKeyValue(i)<getType()!=LEAF)
{
printInConcavo(((CInternalNode*)pNode)->getChild(i), count-2);
}
}
}
void CBPlusTree::printData()const
{
CLeafNode* itr = m_DataHead;
while(itr!=NULL)
{
for (int i=0; igetKeyNum(); ++i)
{
cout<getData(i)<<" ";
}
cout<getRightSibling();
}
}
bool CBPlusTree::remove(KeyType key)
{
if (!search(key)) //不存在
{
return false;
}
if (m_Root->getKeyNum()==1)//特殊情况处理
{
if (m_Root->getType()==LEAF)
{
clear();
return true;
}
else
{
CNode *pChild1 = ((CInternalNode*)m_Root)->getChild(0);
CNode *pChild2 = ((CInternalNode*)m_Root)->getChild(1);
if (pChild1->getKeyNum()==MINNUM_KEY && pChild2->getKeyNum()==MINNUM_KEY)
{
pChild1->mergeChild(m_Root, pChild2, 0);
delete m_Root;
m_Root = pChild1;
}
}
}
recursive_remove(m_Root, key);
return true;
}
// parentNode中包含的键值数>MINNUM_KEY
void CBPlusTree::recursive_remove(CNode* parentNode, KeyType key)
{
int keyIndex = parentNode->getKeyIndex(key);
int childIndex= parentNode->getChildIndex(key, keyIndex); // 孩子结点指针索引
if (parentNode->getType()==LEAF)// 找到目标叶子节点
{
if (key==m_MaxKey&&keyIndex>0)
{
m_MaxKey = parentNode->getKeyValue(keyIndex-1);
}
parentNode->removeKey(keyIndex, childIndex); // 直接删除
// 如果键值在内部结点中存在,也要相应的替换内部结点
if (childIndex==0 && m_Root->getType()!=LEAF && parentNode!=m_DataHead)
{
changeKey(m_Root, key, parentNode->getKeyValue(0));
}
}
else // 内结点
{
CNode *pChildNode = ((CInternalNode*)parentNode)->getChild(childIndex); //包含key的子树根节点
if (pChildNode->getKeyNum()==MINNUM_KEY) // 包含关键字达到下限值,进行相关操作
{
CNode *pLeft = childIndex>0 ? ((CInternalNode*)parentNode)->getChild(childIndex-1) : NULL; //左兄弟节点
CNode *pRight = childIndexgetKeyNum() ? ((CInternalNode*)parentNode)->getChild(childIndex+1) : NULL;//右兄弟节点
// 先考虑从兄弟结点中借
if (pLeft && pLeft->getKeyNum()>MINNUM_KEY)// 左兄弟结点可借
{
pChildNode->borrowFrom(pLeft, parentNode, childIndex-1, LEFT);
}
else if (pRight && pRight->getKeyNum()>MINNUM_KEY)//右兄弟结点可借
{
pChildNode->borrowFrom(pRight, parentNode, childIndex, RIGHT);
}
//左右兄弟节点都不可借,考虑合并
else if (pLeft) //与左兄弟合并
{
pLeft->mergeChild(parentNode, pChildNode, childIndex-1);
pChildNode = pLeft;
}
else if (pRight) //与右兄弟合并
{
pChildNode->mergeChild(parentNode, pRight, childIndex);
}
}
recursive_remove(pChildNode, key);
}
}
void CBPlusTree::changeKey(CNode *pNode, KeyType oldKey, KeyType newKey)
{
if (pNode!=NULL && pNode->getType()!=LEAF)
{
int keyIndex = pNode->getKeyIndex(oldKey);
if (keyIndexgetKeyNum() && oldKey==pNode->getKeyValue(keyIndex)) // 找到
{
pNode->setKeyValue(keyIndex, newKey);
}
else // 继续找
{
changeKey(((CInternalNode*)pNode)->getChild(keyIndex), oldKey, newKey);
}
}
}
bool CBPlusTree::update(KeyType oldKey, KeyType newKey)
{
if (search(newKey)) // 检查更新后的键是否已经存在
{
return false;
}
else
{
int dataValue;
remove(oldKey, dataValue);
if (dataValue==INVALID_INDEX)
{
return false;
}
else
{
return insert(newKey, dataValue);
}
}
}
void CBPlusTree::remove(KeyType key, DataType& dataValue)
{
if (!search(key)) //不存在
{
dataValue = INVALID_INDEX;
return;
}
if (m_Root->getKeyNum()==1)//特殊情况处理
{
if (m_Root->getType()==LEAF)
{
dataValue = ((CLeafNode*)m_Root)->getData(0);
clear();
return;
}
else
{
CNode *pChild1 = ((CInternalNode*)m_Root)->getChild(0);
CNode *pChild2 = ((CInternalNode*)m_Root)->getChild(1);
if (pChild1->getKeyNum()==MINNUM_KEY && pChild2->getKeyNum()==MINNUM_KEY)
{
pChild1->mergeChild(m_Root, pChild2, 0);
delete m_Root;
m_Root = pChild1;
}
}
}
recursive_remove(m_Root, key, dataValue);
}
void CBPlusTree::recursive_remove(CNode* parentNode, KeyType key, DataType& dataValue)
{
int keyIndex = parentNode->getKeyIndex(key);
int childIndex= parentNode->getChildIndex(key, keyIndex); // 孩子结点指针索引
if (parentNode->getType()==LEAF)// 找到目标叶子节点
{
if (key==m_MaxKey&&keyIndex>0)
{
m_MaxKey = parentNode->getKeyValue(keyIndex-1);
}
dataValue = ((CLeafNode*)parentNode)->getData(keyIndex);
parentNode->removeKey(keyIndex, childIndex); // 直接删除
// 如果键值在内部结点中存在,也要相应的替换内部结点
if (childIndex==0 && m_Root->getType()!=LEAF && parentNode!=m_DataHead)
{
changeKey(m_Root, key, parentNode->getKeyValue(0));
}
}
else // 内结点
{
CNode *pChildNode = ((CInternalNode*)parentNode)->getChild(childIndex); //包含key的子树根节点
if (pChildNode->getKeyNum()==MINNUM_KEY) // 包含关键字达到下限值,进行相关操作
{
CNode *pLeft = childIndex>0 ? ((CInternalNode*)parentNode)->getChild(childIndex-1) : NULL; //左兄弟节点
CNode *pRight = childIndexgetKeyNum() ? ((CInternalNode*)parentNode)->getChild(childIndex+1) : NULL;//右兄弟节点
// 先考虑从兄弟结点中借
if (pLeft && pLeft->getKeyNum()>MINNUM_KEY)// 左兄弟结点可借
{
pChildNode->borrowFrom(pLeft, parentNode, childIndex-1, LEFT);
}
else if (pRight && pRight->getKeyNum()>MINNUM_KEY)//右兄弟结点可借
{
pChildNode->borrowFrom(pRight, parentNode, childIndex, RIGHT);
}
//左右兄弟节点都不可借,考虑合并
else if (pLeft) //与左兄弟合并
{
pLeft->mergeChild(parentNode, pChildNode, childIndex-1);
pChildNode = pLeft;
}
else if (pRight) //与右兄弟合并
{
pChildNode->mergeChild(parentNode, pRight, childIndex);
}
}
recursive_remove(pChildNode, key, dataValue);
}
}
vector CBPlusTree::select(KeyType compareKey, int compareOpeartor)
{
vector results;
if (m_Root!=NULL)
{
if (compareKey>m_MaxKey) // 比较键值大于B+树中最大的键值
{
if (compareOpeartor==LE || compareOpeartor==LT)
{
for(CLeafNode* itr = m_DataHead; itr!=NULL; itr= itr->getRightSibling())
{
for (int i=0; igetKeyNum(); ++i)
{
results.push_back(itr->getData(i));
}
}
}
}
else if (compareKeygetKeyValue(0)) // 比较键值小于B+树中最小的键值
{
if (compareOpeartor==BE || compareOpeartor==BT)
{
for(CLeafNode* itr = m_DataHead; itr!=NULL; itr= itr->getRightSibling())
{
for (int i=0; igetKeyNum(); ++i)
{
results.push_back(itr->getData(i));
}
}
}
}
else // 比较键值在B+树中
{
SelectResult result;
search(compareKey, result);
switch(compareOpeartor)
{
case LT:
case LE:
{
CLeafNode* itr = m_DataHead;
int i;
while (itr!=result.targetNode)
{
for (i=0; igetKeyNum(); ++i)
{
results.push_back(itr->getData(i));
}
itr = itr->getRightSibling();
}
for (i=0; igetData(i));
}
if (itr->getKeyValue(i)getKeyValue(i)))
{
results.push_back(itr->getData(i));
}
}
break;
case EQ:
{
if (result.targetNode->getKeyValue(result.keyIndex)==compareKey)
{
results.push_back(result.targetNode->getData(result.keyIndex));
}
}
break;
case BE:
case BT:
{
CLeafNode* itr = result.targetNode;
if (compareKeygetKeyValue(result.keyIndex) ||
(compareOpeartor==BE && compareKey==itr->getKeyValue(result.keyIndex))
)
{
results.push_back(itr->getData(result.keyIndex));
}
int i;
for (i=result.keyIndex+1; igetKeyNum(); ++i)
{
results.push_back(itr->getData(i));
}
itr = itr->getRightSibling();
while (itr!=NULL)
{
for (i=0; igetKeyNum(); ++i)
{
results.push_back(itr->getData(i));
}
itr = itr->getRightSibling();
}
}
break;
default: // 范围查询
break;
}
}
}
sort::iterator>(results.begin(), results.end());
return results;
}
vector CBPlusTree::select(KeyType smallKey, KeyType largeKey)
{
vector results;
if (smallKey<=largeKey)
{
SelectResult start, end;
search(smallKey, start);
search(largeKey, end);
CLeafNode* itr = start.targetNode;
int i = start.keyIndex;
if (itr->getKeyValue(i)getKeyValue(end.keyIndex)>largeKey)
{
--end.keyIndex;
}
while (itr!=end.targetNode)
{
for (; igetKeyNum(); ++i)
{
results.push_back(itr->getData(i));
}
itr = itr->getRightSibling();
i = 0;
}
for (; i<=end.keyIndex; ++i)
{
results.push_back(itr->getData(i));
}
}
sort::iterator>(results.begin(), results.end());
return results;
}
void CBPlusTree::search(KeyType key, SelectResult& result)
{
recursive_search(m_Root, key, result);
}
void CBPlusTree::recursive_search(CNode* pNode, KeyType key, SelectResult& result)
{
int keyIndex = pNode->getKeyIndex(key);
int childIndex = pNode->getChildIndex(key, keyIndex); // 孩子结点指针索引
if (pNode->getType()==LEAF)
{
result.keyIndex = keyIndex;
result.targetNode = (CLeafNode*)pNode;
return;
}
else
{
return recursive_search(((CInternalNode*)pNode)->getChild(childIndex), key, result);
}
}
红黑树
https://www.cnblogs.com/sgatb...
性质
在一棵红黑树中,其每个结点上增加了一个存储位(属性color)来表示结点的颜色,且颜色只能是red or black。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是近似于平衡的。
树中每个结点包含5个属性:color、val、lchild、rchild和p(可选)。如果一个结点没有子结点或父结点,则该结点相应指针属性的值为NIL。我们可以把这些NIL视为指向二叉搜索树的叶结点(外部结点)的指针,而把带关键字的结点视为树的内部结点。
一棵红黑树是满足下面红黑性质的二叉搜索树:
- 每个结点或是红色的,或是黑色的。
- 根结点是黑色的。
- 每个叶结点(NIL)是黑色的。
- 如果一个结点是红色的,则它的两个子结点都是黑色的。
- 对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
为了便于处理红黑树代码中的边界条件,使用一个哨兵来代表NIL。对于一棵红黑树tree,哨兵NIL是与一个与树中普通结点有相同属性的对象。它的color属性为black,其他属性可以为任意值。
旋转
在一棵含有n个关键字的红黑树上,进行插入和删除操作,需要的时间复杂度为O(logn),由于这两个操作,会导致插入和删除后的树不满足红黑树的性质。为了维护这些性质,需要改变树中某些结点的颜色以及指针结构。
指针结构的修改是通过旋转来完成的,这是一种能保持二叉搜索树性质的搜索树局部操作,旋转分为左旋和右旋。如下图所示:
下面给出左旋和右旋操作的代码为:
Code
template
void RedBlackTree::LeftRotation(RedBlackNode* &t){
RedBlackNode *temp = t->rchild;
t->rchild = temp->lchild;
if(Parent(t)==NIL){
root = temp;
}
temp->lchild = t;
Parent(t)->rchild = temp;
}
template
void RedBlackTree::RightRotation(RedBlackNode* &t){
RedBlackNode *temp = t->lchild;
t->lchild = temp->rchild;
if(Parent(t)==NIL){
root = temp;
}
temp->rchild = t;
Parent(t)->lchild = temp;
}
插入
前面说过,在一棵含有n个关键字的红黑树上,执行插入操作,需要时间复杂度为O(logn)。为了做到这一点,需要往原红黑树中插入一个红色的结点。那么问题来了,为什么插入的是红色结点,而不是黑色结点呢?我们知道,红黑树有五个性质,如果插入红色结点,则可能会违反性质4,而如果是插入黑色结点,则一定会违反性质5。也就是说,插入红色结点比插入黑色结点更不容易违反红黑树的性质,而违反性质条数越多,相应的要做的调整操作也就越多,导致算法的时间复杂度也就越高,从而影响算法的执行速度。在《数据结构算法与解析》(高一凡著,清华大学出版社)一书中,给出了插入结点为红色以及插入结点为黑色两种操作的算法,本文以插入结点为红色进行讲解。
对于一棵红黑树来说,插入一个红色结点,主要有六种情况,其中三种与另外三种是对称的。这一点取决于插入结点 z 的父亲结点是插入结点的祖父结点的左孩子还是右孩子。
下面给出两种对称下,所对应的三种情况:
- case1:插入结点 z 的叔结点 y 是红色的。
上图显示了该情况的情形,这种情况实在插入结点z的父结点z.p和其叔结点y都是红色时发生的。因为插入结点z的祖父结点z.p.p是黑色的,所以将z.p和y都着为黑色,来解决z和z.p都是红色的问题,而由于性质5的要求,如果只是把z.p和y的颜色改为黑色,则会破坏该性质,因此需要对z.p.p结点的颜色进行调整,以保证性质5的满足。
但是,性质1调整以后,就一定能维持红黑树的性质吗?我们以z表示新插入的结点,z'表示经过此次操作后的结点,由上述操作可以知道,z'=z.p.p。则经过此次操作后,有以下结果:
-
- 因为这次操作把z.p.p着为红色,结点z'在下次操作的开始是红色的。
- 在这次操作中结点z'.p是z.p.p.p,且这个结点的颜色不会改变。如果它是根结点,则在此次迭代之前它是黑色的,且它在下次操作的开头任然是黑色的。
- 我们也知道,case1保持性质5,而且它也不会引起性质1或性质3的破坏。
如果结点z'在下一次操作开始时是根结点,则在这次操作中case1修正了唯一被破坏的性质4。由于z'是红色的而且是根结点,所以性质2成为唯一被违反的性质,这是由z'导致的。
如果结点z'在下一次操作开始时不是根结点,则case1不会导致性质2被破坏,case1修正了在这次操作的开始唯一违反的性质4。然后把z’着色为红色而z'.p不变,因此,如果z'.p是黑色的,则没有违反性质4,若是z'.p是红色的,则违反了性质4。
- case2:插入结点 z 的叔结点 y 是黑色的且 z 是一个右孩子。
- case3:插入结点 z 的叔结点 y 是黑色的且 z 是一个左孩子。
在case2和case3中,z的叔结点是黑色的。通过z是z.p的右孩子还是左孩子来区别这两种情况(叔结点都是黑色,无法在逻辑上进行区别)。对于这两种情况,如下图所示:
左图为case2,右图为case3
我们发现case2与case3存在某种指针结构上的关系,很明显二者之间可以通过左旋和右旋操作进行相互转换。由于z和z.p都是红色的,因此,旋转操作对树的黑高和性质5都无影响。无论怎么进入哪两种情况,y总是黑色的,否则就要执行case1对应的操作。此外,这种旋转操作,有个好处是,并不改变旋转后,z的身份,尽管会导致z的左右孩子身份改变了,但依旧是z.p的孩子。在case3中,我们可以通过改变某些结点的颜色,并作一次右旋,就能保证性质5。这样,由于在一行中不会再存在有两个红色结点,因此,保证了红黑树的性质,所有的处理也到此完毕了。如下所示:
可以看到,case2和case3的操作,会最终使得插入结点后的树,维持红黑树的性质。由此,不禁怀疑,这样的操作能完全保证吗?答案是肯定的。下面来证明:
- case2让z指向红色的z.p。在case2和case3中,z或z的颜色都不会改变,所以,在由case2转为case3后,这并不会产生其他性质的改变。
- case3把z.p着成黑色,使得如果z.p在下一次操作开始时是根结点,则它是黑色的。
- 和case1一样,红黑树的性质1、3、5都在case2和case3中得以保持。
由于结点z在case2和case3中都不是根结点,因此,性质2未被破坏,这两种情况因此也不会引起性质2的违反。由此,证明了z.p为z.p.p的左孩子时候,对插入z后的红黑树,按照上述调整,可以做到恢复红黑树的性质。而当z.p为z.p.p的右孩子时,由于与前面一大情况是对称的,因此,通过修改left和right的对应,就可实现。而完全实现树的回复,可以通过while循环来保持。以下是实现树的插入的代码:
template
bool RedBlackTree::Insert(T e){
RedBlackNode *p, *f;
p = f = NULL;
if(!searchBST(root, p, e, f)){//not found, need to create, p points to the last node.
RedBlackNode *s = createNewNode(e);
if(root==NULL){
root = s;
root->color = "black";
}
else{
if(eval){
p->lchild = s;
}
else{
p->rchild = s;
}
if(p->color == "red"){//double red node, need to adjust
adjustDoubleRed(s, p);
}
}
return true;
}
else{//node exists. return false
return false;
}
}
template
RedBlackNode* RedBlackTree::Parent(RedBlackNode* &t)const{
/*
*@Parameter:
*q: a queue to save rb-node.
*t: a point which points to a node in the RBTree.
*father: a point which points to the father node of t.
*/
queue*> q;
RedBlackNode* father;
if(root!=NULL){
q.push(root);
while(!q.empty()){//BFSTraverse to find the father node of t.
father = q.front();
q.pop();
if((father->lchild!=NIL&&father->lchild==t)||(father->rchild!=NIL&&father->rchild==t)){
return father;
}
else{
if(father->lchild!=NIL){
q.push(father->lchild);
}
if(father->rchild!=NIL){
q.push(father->rchild);
}
}
}
}
return NIL; //not found, return NIL
}
template
bool RedBlackTree::searchBST(RedBlackNode* &t, RedBlackNode* &p, T &e, RedBlackNode* f)const{
//在树中t中递归地查找其值等于e的数据,若查找成功,则指针p指向该数据
//结点,并返回true,否则指针p指向查找路径上访问的最后一个结点以便插入
//并返回false,指针f指向p的双亲,其初始调用值为NULL。Insert()调用
if(t==NULL||t==NIL){
p = f;
return false;
}
if(e==t->val){
p = t;
return true;
}
else if(eval){
return searchBST(t->lchild, p, e, t);
}
else{
return searchBST(t->rchild, p, e, t);
}
}
template
void RedBlackTree::LeftRotation(RedBlackNode* &t){
RedBlackNode *temp = t->rchild;
t->rchild = temp->lchild;
if(Parent(t)==NIL){
root = temp;
}
temp->lchild = t;
Parent(t)->rchild = temp;
}
template
void RedBlackTree::RightRotation(RedBlackNode* &t){
RedBlackNode *temp = t->lchild;
t->lchild = temp->rchild;
if(Parent(t)==NIL){
root = temp;
}
temp->rchild = t;
Parent(t)->lchild = temp;
}
template
void RedBlackTree::adjustDoubleRed(RedBlackNode* &s, RedBlackNode* &p){
/*
*@Parameter:
*s: rb-node.
*p: the father node of s.
*/
RedBlackNode *y, *gp;
while(p->color=="red"){
gp = Parent(p);
if(p==gp->lchild){
y = gp->rchild;
if(y->color=="red"){//case 1
p->color = "black";
y->color = "black";
gp->color = "red";
s = gp;
p = Parent(s);
}
else if(s==p->rchild){//case 2
s = p;
LeftRotation(p);
}
else{
p->color = "black";
gp->color = "red";
RightRotation(gp);
}
}
else{
y = gp->lchild;
if(y->color=="red"){//case 1
p->color = "black";
y->color = "black";
gp->color = "red";
s = gp;
p = Parent(s);
}
else if(s==p->lchild){//case 2
s = p;
RightRotation(s);
}
else{
p->color = "black";
gp->color = "red";
LeftRotation(gp);
}
}
}
root->color = "black";
}
删除
由于红黑树与BST树相似,因此,其删除操作与BST树在逻辑上是基本一致的,唯一的区别在于,红黑树需要对删除结点后的树进行调整,使其符合红黑树的性质。对于一棵红黑树来说,如果先不考虑结点的颜色,删除一个结点无非是三种情况,这一点与BST树是一致的,即:
- 被删除结点没有左右子结点;
- 被删除结点仅有一个子节点(左或右都有可能);
- 被删除结点左右子结点都存在;
根据上述三种情况,可以编写出BST树的删除结点操作的代码,下面给出BST树的删除操作示意图:
很明显,红黑树在结点的结构上,也是符合上述形式的,即左<根<右,因此,红黑树的删除操作是从BST输的删除操作的基础上,修改得到的,为什么需要修改呢?就是因为红黑树的每个结点具有红黑属性。
由于红黑属性的影响,导致,删除结点后红黑树将不符合红黑树原有的特性,我们知道,删除某个结点,按照上述调整,将会使得被删除结点所在的子树不符合原红黑树的特性1、2、4或5(非删除结点不受影响)。因此,只需要对子树进行颜色调整,就能使红黑树性质保持不变。
伪代码中Transplant函数实现
如何删除的原理已经讲明白了,那么我们看,两个结点是如何替换(也就是发生删除操作的)。
在伪码中,结点u为被替换结点,你可以理解为,被删除结点,而v是用来替换被删除结点的结点(通常为u的子节点或者u的右子树结点的最小节点)。
Code
template
void RedBlackTree::Transplant(RedBlackNode* &u, RedBlackNode* &v){
/*
*a function to achieve node u is replaced by v.
*@Parameter:
*u: a node which is replaced by v.
*v: a node wants to replace u.
*/
if(Parent(u) == NIL){//待删除结点为根结点.
root = v;
}
else if(u==Parent(u)->lchild){
Parent(u)->lchild = v;
}
else{
Parent(u)->rchild = v;
}
}
具体实现
下面给出删除操作的伪代码(源自《算法导论》)。
在上述代码中,结点z为删除结点,y为指向结点z的指针。我们知道,BST的删除操作是很容易实现的,对于红黑树来说,关键在于,删除操作以后,什么情况下,会破坏红黑树的红黑性质。
由于y的颜色有可能发生改变(因为根据代码,y始终指向树结构中被删除结点的位置),用变量y_original_color存储了发生改变前的y位置的颜色。第2行和第10行在给y赋值之后,立即设置该变量。当z有两个子结点时,则y!=z且结点y移至红黑树中结点z的原始位置;第20行给y赋予和z一样的颜色。然后保存y的原始颜色,以在删除操作结束时,测试它;如果它是黑色的,那么删除或移动y会引起红黑性质的破坏,为什么会是黑色引起红黑性质的破坏呢?
- 对于情况1,即不存在子结点的被删除结点来说,什么情况下删除该结点以后会改变原有红黑树的性质呢?很显然,被删除结点是黑色的时候,删除它会违背红黑树的性质5,而被删除结点为红色的时候,删除它并不会影响红黑树的性质,直接修改即可(如下所示),在这里,就产生了删除结点后,后续修改颜色的第一种情况。
如上图所示,如果删除结点5,对于左侧的树,如果删除结点y,则结点7不会违反任何红黑树的性质,因为结点y的子结点为必定为黑色(由于红黑树的性质),因此,y为红色不会引起红黑树性质的改变;对于右侧的树,如果删除结点y,则如果结点y的子结点为NIL(黑色),不会引起结点7与结点y子结点之间都为红色,从而不违反了红黑树的性质。
- 对于情况2,即存在左结点或者右结点,由于红黑树结点存在color属性,因此,常见的做法是将用来替换删除结点的结点的颜色改成与删除结点颜色一致,这样,只需要对修改过指针结构后的子树进行修改其颜色,即可完成红黑树性质的保持。那么,由于颜色的存在,又会有那些情况的出现呢?我们知道,一个结点无非是红色或者黑色,且根据性质,红结点的子结点颜色必为黑色,那么,以被删除结点为左结点为例,有下面两种情况:
从左边的红黑树来看,如果待删除结点颜色为黑色,当对该结点进行操作时,则由于,其子结点为红色,与y结点的父结点同为红色,因此,会违背红黑树的性质,而如果是右边的情况,则不会,因为删除结点y以后,由于结点4依旧为黑色,不会破坏红黑树的性质。对于这种情况下,左子树结点不存在而右子树结点存在的情况,也是同样的道理,读者可以自己画图思考一下。
- 对于情况3,即被删除结点同时存在左右子结点,如下图所示:
从上图来看,如果删除结点5,会与情况2一样而违反性质,而对于右边的树,则不会,因为,我们删除的方式,是将z结点也就是结点5的左子树,连接到结点5右子树的值最小的结点上。然后用这个最小结点来替换原来结点z(也就是图上结点5的位置)。这样做的好处是,仍可以保证删除后的树仍满足BST树的性质,我们只需要对被删除结点的子树进行修改颜色性质就可以了,而且,不论最小结点的颜色如何,都不会导致出现两个红结点的情况。这一点可能很多人会存在疑惑,我们来分析一下:
对于左右子树都存在的的删除结点来说,此时,y从指向z转向了指向删除结点z的右子树种最小的一个结点(右子树的最左结点),这样的指向,无非两种情况,一种是右子树的最左结点就是被删除结点z的右孩子,即其右孩子的左孩子为NIL,这样,以上右图为例,y指向了结点6,然6后,用y_original_color来保存结点6的颜色,用x指向6的右孩子,由于在这里,y的父亲结点依旧为删除结点z,因此,设置好结点属性x.p = y,然后,执行替换操作(Transplant)来实现删除的目的,可以看到,transplant操作是对删除结点z和替换结点y进行操作的,对于这种情况来说,是将z与其右孩子进行替换,根据伪码,结点7的左孩子指向了结点6。然后结点6也就是现在的结点y的左孩子指向了结点z的左孩子,这样就完成了然后将结点y的颜色,改成结点z的颜色,为什么这么做呢,是为了保持与原来红黑树相同的特性,因为我们知道,在删除结点5之前,结点5左右两棵子树的一条路径上的黑色结点数目是相同的,但是,由于结点6的上位(替换了其父结点),而父结点的左孩子直接成为其左子树,这就导致了左右两子树的不平衡,调整为与z结点相同的颜色以后,可以使得对红黑树修改操作仅局限于结点y这棵子树中进行。
另一种情况则是右子树的最左结点不是被删除结点z的右孩子,即其右孩子的左孩子非空,那么,其余操作不变,比较巧妙的是,这里运用了最左结点的左孩子为NIL的特性,将最左结点的右子树接到了其父节点的左边(这就保证了BST树的有序性),且这样的做法,使得该结点与树在结构上断开,无法通过树的根结点访问到,因此,后续再执行一次删除结点z与结点y的替换操作,就可以完成这样的删除操作。这种情况下,会产生y为红色或者黑色结点的问题,同样,也只会有黑色会违背红黑树的性质。
删除操作之后改变颜色
通过上面的分析,我们很容易得到这样的结论,那就是,只有当y为黑色结点的时候,才会发生违背红黑树性质的情况,因此,需要对这样的情况进行修改,来保持红黑树的性质。下面给出《算法导论》中,关于该操作的伪代码:
在分析源码以前,我们首先来分析,执行删除操作以后,会出现哪些违背红黑树性质的情况。在这个操作中,结合删除操作的代码,我们可以发现,x始终指向具有双重黑色的非根结点。那么,就会有4种情况,这四种情况与插入操作中的是类似的,请读者结合上述删除操作,自行分析。
Code
template
void RedBlackTree::Delete(RedBlackNode* &t){
/*
*function to delete node t in redblacktree.
*@Parameter:
*t: a node need to be deleted.
*/
RedBlackNode *y;
RedBlackNode *p;
y = t;
string y_original_color = y->color;
if(t->lchild==NIL){
p = t->rchild;
Transplant(t, t->rchild);
}
else if(t->rchild==NIL){
p = t->lchild;
Transplant(t, t->lchild);
}
else{
y = TreeMinimum(t->rchild);
y_original_color = y->color;
p = y->rchild;
if(Parent(y)!=t){
Transplant(y, y->rchild);
y->rchild = t->rchild;
}
Transplant(t, y);
y->lchild = t->lchild;
y->color = t->color;
}
if(y_original_color=="black"){
RBDeleteFixup(p);
}
delete t;
t = NULL;
}
Code
https://www.cnblogs.com/skywa...
(推荐)
//RBTree.h
/**
* C++ 语言: 红黑树
*
* @author skywang
* @date 2013/11/07
*/
#ifndef _RED_BLACK_TREE_HPP_
#define _RED_BLACK_TREE_HPP_
#include
#include
using namespace std;
enum RBTColor{RED, BLACK};
template
class RBTNode{
public:
RBTColor color; // 颜色
T key; // 关键字(键值)
RBTNode *left; // 左孩子
RBTNode *right; // 右孩子
RBTNode *parent; // 父结点
RBTNode(T value, RBTColor c, RBTNode *p, RBTNode *l, RBTNode *r):
key(value),color(c),parent(),left(l),right(r) {}
};
template
class RBTree {
private:
RBTNode *mRoot; // 根结点
public:
RBTree();
~RBTree();
// 前序遍历"红黑树"
void preOrder();
// 中序遍历"红黑树"
void inOrder();
// 后序遍历"红黑树"
void postOrder();
// (递归实现)查找"红黑树"中键值为key的节点
RBTNode* search(T key);
// (非递归实现)查找"红黑树"中键值为key的节点
RBTNode* iterativeSearch(T key);
// 查找最小结点:返回最小结点的键值。
T minimum();
// 查找最大结点:返回最大结点的键值。
T maximum();
// 找结点(x)的后继结点。即,查找"红黑树中数据值大于该结点"的"最小结点"。
RBTNode* successor(RBTNode *x);
// 找结点(x)的前驱结点。即,查找"红黑树中数据值小于该结点"的"最大结点"。
RBTNode* predecessor(RBTNode *x);
// 将结点(key为节点键值)插入到红黑树中
void insert(T key);
// 删除结点(key为节点键值)
void remove(T key);
// 销毁红黑树
void destroy();
// 打印红黑树
void print();
private:
// 前序遍历"红黑树"
void preOrder(RBTNode* tree) const;
// 中序遍历"红黑树"
void inOrder(RBTNode* tree) const;
// 后序遍历"红黑树"
void postOrder(RBTNode* tree) const;
// (递归实现)查找"红黑树x"中键值为key的节点
RBTNode* search(RBTNode* x, T key) const;
// (非递归实现)查找"红黑树x"中键值为key的节点
RBTNode* iterativeSearch(RBTNode* x, T key) const;
// 查找最小结点:返回tree为根结点的红黑树的最小结点。
RBTNode* minimum(RBTNode* tree);
// 查找最大结点:返回tree为根结点的红黑树的最大结点。
RBTNode* maximum(RBTNode* tree);
// 左旋
void leftRotate(RBTNode* &root, RBTNode* x);
// 右旋
void rightRotate(RBTNode* &root, RBTNode* y);
// 插入函数
void insert(RBTNode* &root, RBTNode* node);
// 插入修正函数
void insertFixUp(RBTNode* &root, RBTNode* node);
// 删除函数
void remove(RBTNode* &root, RBTNode *node);
// 删除修正函数
void removeFixUp(RBTNode* &root, RBTNode *node, RBTNode *parent);
// 销毁红黑树
void destroy(RBTNode* &tree);
// 打印红黑树
void print(RBTNode* tree, T key, int direction);
#define rb_parent(r) ((r)->parent)
#define rb_color(r) ((r)->color)
#define rb_is_red(r) ((r)->color==RED)
#define rb_is_black(r) ((r)->color==BLACK)
#define rb_set_black(r) do { (r)->color = BLACK; } while (0)
#define rb_set_red(r) do { (r)->color = RED; } while (0)
#define rb_set_parent(r,p) do { (r)->parent = (p); } while (0)
#define rb_set_color(r,c) do { (r)->color = (c); } while (0)
};
/*
* 构造函数
*/
template
RBTree::RBTree():mRoot(NULL)
{
mRoot = NULL;
}
/*
* 析构函数
*/
template
RBTree::~RBTree()
{
destroy();
}
/*
* 前序遍历"红黑树"
*/
template
void RBTree::preOrder(RBTNode* tree) const
{
if(tree != NULL)
{
cout<< tree->key << " " ;
preOrder(tree->left);
preOrder(tree->right);
}
}
template
void RBTree::preOrder()
{
preOrder(mRoot);
}
/*
* 中序遍历"红黑树"
*/
template
void RBTree::inOrder(RBTNode* tree) const
{
if(tree != NULL)
{
inOrder(tree->left);
cout<< tree->key << " " ;
inOrder(tree->right);
}
}
template
void RBTree::inOrder()
{
inOrder(mRoot);
}
/*
* 后序遍历"红黑树"
*/
template
void RBTree::postOrder(RBTNode* tree) const
{
if(tree != NULL)
{
postOrder(tree->left);
postOrder(tree->right);
cout<< tree->key << " " ;
}
}
template
void RBTree::postOrder()
{
postOrder(mRoot);
}
/*
* (递归实现)查找"红黑树x"中键值为key的节点
*/
template
RBTNode* RBTree::search(RBTNode* x, T key) const
{
if (x==NULL || x->key==key)
return x;
if (key < x->key)
return search(x->left, key);
else
return search(x->right, key);
}
template
RBTNode* RBTree::search(T key)
{
search(mRoot, key);
}
/*
* (非递归实现)查找"红黑树x"中键值为key的节点
*/
template
RBTNode* RBTree::iterativeSearch(RBTNode* x, T key) const
{
while ((x!=NULL) && (x->key!=key))
{
if (key < x->key)
x = x->left;
else
x = x->right;
}
return x;
}
template
RBTNode* RBTree::iterativeSearch(T key)
{
iterativeSearch(mRoot, key);
}
/*
* 查找最小结点:返回tree为根结点的红黑树的最小结点。
*/
template
RBTNode* RBTree::minimum(RBTNode* tree)
{
if (tree == NULL)
return NULL;
while(tree->left != NULL)
tree = tree->left;
return tree;
}
template
T RBTree::minimum()
{
RBTNode *p = minimum(mRoot);
if (p != NULL)
return p->key;
return (T)NULL;
}
/*
* 查找最大结点:返回tree为根结点的红黑树的最大结点。
*/
template
RBTNode* RBTree::maximum(RBTNode* tree)
{
if (tree == NULL)
return NULL;
while(tree->right != NULL)
tree = tree->right;
return tree;
}
template
T RBTree::maximum()
{
RBTNode *p = maximum(mRoot);
if (p != NULL)
return p->key;
return (T)NULL;
}
/*
* 找结点(x)的后继结点。即,查找"红黑树中数据值大于该结点"的"最小结点"。
*/
template
RBTNode* RBTree::successor(RBTNode *x)
{
// 如果x存在右孩子,则"x的后继结点"为 "以其右孩子为根的子树的最小结点"。
if (x->right != NULL)
return minimum(x->right);
// 如果x没有右孩子。则x有以下两种可能:
// (01) x是"一个左孩子",则"x的后继结点"为 "它的父结点"。
// (02) x是"一个右孩子",则查找"x的最低的父结点,并且该父结点要具有左孩子",找到的这个"最低的父结点"就是"x的后继结点"。
RBTNode* y = x->parent;
while ((y!=NULL) && (x==y->right))
{
x = y;
y = y->parent;
}
return y;
}
/*
* 找结点(x)的前驱结点。即,查找"红黑树中数据值小于该结点"的"最大结点"。
*/
template
RBTNode* RBTree::predecessor(RBTNode *x)
{
// 如果x存在左孩子,则"x的前驱结点"为 "以其左孩子为根的子树的最大结点"。
if (x->left != NULL)
return maximum(x->left);
// 如果x没有左孩子。则x有以下两种可能:
// (01) x是"一个右孩子",则"x的前驱结点"为 "它的父结点"。
// (01) x是"一个左孩子",则查找"x的最低的父结点,并且该父结点要具有右孩子",找到的这个"最低的父结点"就是"x的前驱结点"。
RBTNode* y = x->parent;
while ((y!=NULL) && (x==y->left))
{
x = y;
y = y->parent;
}
return y;
}
/*
* 对红黑树的节点(x)进行左旋转
*
* 左旋示意图(对节点x进行左旋):
* px px
* / /
* x y
* / \ --(左旋)--> / \ #
* lx y x ry
* / \ / \
* ly ry lx ly
*
*
*/
template
void RBTree::leftRotate(RBTNode* &root, RBTNode* x)
{
// 设置x的右孩子为y
RBTNode *y = x->right;
// 将 “y的左孩子” 设为 “x的右孩子”;
// 如果y的左孩子非空,将 “x” 设为 “y的左孩子的父亲”
x->right = y->left;
if (y->left != NULL)
y->left->parent = x;
// 将 “x的父亲” 设为 “y的父亲”
y->parent = x->parent;
if (x->parent == NULL)
{
root = y; // 如果 “x的父亲” 是空节点,则将y设为根节点
}
else
{
if (x->parent->left == x)
x->parent->left = y; // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
else
x->parent->right = y; // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
}
// 将 “x” 设为 “y的左孩子”
y->left = x;
// 将 “x的父节点” 设为 “y”
x->parent = y;
}
/*
* 对红黑树的节点(y)进行右旋转
*
* 右旋示意图(对节点y进行左旋):
* py py
* / /
* y x
* / \ --(右旋)--> / \ #
* x ry lx y
* / \ / \ #
* lx rx rx ry
*
*/
template
void RBTree::rightRotate(RBTNode* &root, RBTNode* y)
{
// 设置x是当前节点的左孩子。
RBTNode *x = y->left;
// 将 “x的右孩子” 设为 “y的左孩子”;
// 如果"x的右孩子"不为空的话,将 “y” 设为 “x的右孩子的父亲”
y->left = x->right;
if (x->right != NULL)
x->right->parent = y;
// 将 “y的父亲” 设为 “x的父亲”
x->parent = y->parent;
if (y->parent == NULL)
{
root = x; // 如果 “y的父亲” 是空节点,则将x设为根节点
}
else
{
if (y == y->parent->right)
y->parent->right = x; // 如果 y是它父节点的右孩子,则将x设为“y的父节点的右孩子”
else
y->parent->left = x; // (y是它父节点的左孩子) 将x设为“x的父节点的左孩子”
}
// 将 “y” 设为 “x的右孩子”
x->right = y;
// 将 “y的父节点” 设为 “x”
y->parent = x;
}
/*
* 红黑树插入修正函数
*
* 在向红黑树中插入节点之后(失去平衡),再调用该函数;
* 目的是将它重新塑造成一颗红黑树。
*
* 参数说明:
* root 红黑树的根
* node 插入的结点 // 对应《算法导论》中的z
*/
template
void RBTree::insertFixUp(RBTNode* &root, RBTNode* node)
{
RBTNode *parent, *gparent;
// 若“父节点存在,并且父节点的颜色是红色”
while ((parent = rb_parent(node)) && rb_is_red(parent))
{
gparent = rb_parent(parent);
//若“父节点”是“祖父节点的左孩子”
if (parent == gparent->left)
{
// Case 1条件:叔叔节点是红色
{
RBTNode *uncle = gparent->right;
if (uncle && rb_is_red(uncle))
{
rb_set_black(uncle);
rb_set_black(parent);
rb_set_red(gparent);
node = gparent;
continue;
}
}
// Case 2条件:叔叔是黑色,且当前节点是右孩子
if (parent->right == node)
{
RBTNode *tmp;
leftRotate(root, parent);
tmp = parent;
parent = node;
node = tmp;
}
// Case 3条件:叔叔是黑色,且当前节点是左孩子。
rb_set_black(parent);
rb_set_red(gparent);
rightRotate(root, gparent);
}
else//若“z的父节点”是“z的祖父节点的右孩子”
{
// Case 1条件:叔叔节点是红色
{
RBTNode *uncle = gparent->left;
if (uncle && rb_is_red(uncle))
{
rb_set_black(uncle);
rb_set_black(parent);
rb_set_red(gparent);
node = gparent;
continue;
}
}
// Case 2条件:叔叔是黑色,且当前节点是左孩子
if (parent->left == node)
{
RBTNode *tmp;
rightRotate(root, parent);
tmp = parent;
parent = node;
node = tmp;
}
// Case 3条件:叔叔是黑色,且当前节点是右孩子。
rb_set_black(parent);
rb_set_red(gparent);
leftRotate(root, gparent);
}
}
// 将根节点设为黑色
rb_set_black(root);
}
/*
* 将结点插入到红黑树中
*
* 参数说明:
* root 红黑树的根结点
* node 插入的结点 // 对应《算法导论》中的node
*/
template
void RBTree::insert(RBTNode* &root, RBTNode* node)
{
RBTNode *y = NULL;
RBTNode *x = root;
// 1. 将红黑树当作一颗二叉查找树,将节点添加到二叉查找树中。
while (x != NULL)
{
y = x;
if (node->key < x->key)
x = x->left;
else
x = x->right;
}
node->parent = y;
if (y!=NULL)
{
if (node->key < y->key)
y->left = node;
else
y->right = node;
}
else
root = node;
// 2. 设置节点的颜色为红色
node->color = RED;
// 3. 将它重新修正为一颗二叉查找树
insertFixUp(root, node);
}
/*
* 将结点(key为节点键值)插入到红黑树中
*
* 参数说明:
* tree 红黑树的根结点
* key 插入结点的键值
*/
template
void RBTree::insert(T key)
{
RBTNode *z=NULL;
// 如果新建结点失败,则返回。
if ((z=new RBTNode(key,BLACK,NULL,NULL,NULL)) == NULL)
return ;
insert(mRoot, z);
}
/*
* 红黑树删除修正函数
*
* 在从红黑树中删除插入节点之后(红黑树失去平衡),再调用该函数;
* 目的是将它重新塑造成一颗红黑树。
*
* 参数说明:
* root 红黑树的根
* node 待修正的节点
*/
template
void RBTree::removeFixUp(RBTNode* &root, RBTNode *node, RBTNode *parent)
{
RBTNode *other;
while ((!node || rb_is_black(node)) && node != root)
{
if (parent->left == node)
{
other = parent->right;
if (rb_is_red(other))
{
// Case 1: x的兄弟w是红色的
rb_set_black(other);
rb_set_red(parent);
leftRotate(root, parent);
other = parent->right;
}
if ((!other->left || rb_is_black(other->left)) &&
(!other->right || rb_is_black(other->right)))
{
// Case 2: x的兄弟w是黑色,且w的俩个孩子也都是黑色的
rb_set_red(other);
node = parent;
parent = rb_parent(node);
}
else
{
if (!other->right || rb_is_black(other->right))
{
// Case 3: x的兄弟w是黑色的,并且w的左孩子是红色,右孩子为黑色。
rb_set_black(other->left);
rb_set_red(other);
rightRotate(root, other);
other = parent->right;
}
// Case 4: x的兄弟w是黑色的;并且w的右孩子是红色的,左孩子任意颜色。
rb_set_color(other, rb_color(parent));
rb_set_black(parent);
rb_set_black(other->right);
leftRotate(root, parent);
node = root;
break;
}
}
else
{
other = parent->left;
if (rb_is_red(other))
{
// Case 1: x的兄弟w是红色的
rb_set_black(other);
rb_set_red(parent);
rightRotate(root, parent);
other = parent->left;
}
if ((!other->left || rb_is_black(other->left)) &&
(!other->right || rb_is_black(other->right)))
{
// Case 2: x的兄弟w是黑色,且w的俩个孩子也都是黑色的
rb_set_red(other);
node = parent;
parent = rb_parent(node);
}
else
{
if (!other->left || rb_is_black(other->left))
{
// Case 3: x的兄弟w是黑色的,并且w的左孩子是红色,右孩子为黑色。
rb_set_black(other->right);
rb_set_red(other);
leftRotate(root, other);
other = parent->left;
}
// Case 4: x的兄弟w是黑色的;并且w的右孩子是红色的,左孩子任意颜色。
rb_set_color(other, rb_color(parent));
rb_set_black(parent);
rb_set_black(other->left);
rightRotate(root, parent);
node = root;
break;
}
}
}
if (node)
rb_set_black(node);
}
/*
* 删除结点(node),并返回被删除的结点
*
* 参数说明:
* root 红黑树的根结点
* node 删除的结点
*/
template
void RBTree::remove(RBTNode* &root, RBTNode *node)
{
RBTNode *child, *parent;
RBTColor color;
// 被删除节点的"左右孩子都不为空"的情况。
if ( (node->left!=NULL) && (node->right!=NULL) )
{
// 被删节点的后继节点。(称为"取代节点")
// 用它来取代"被删节点"的位置,然后再将"被删节点"去掉。
RBTNode *replace = node;
// 获取后继节点
replace = replace->right;
while (replace->left != NULL)
replace = replace->left;
// "node节点"不是根节点(只有根节点不存在父节点)
if (rb_parent(node))
{
if (rb_parent(node)->left == node)
rb_parent(node)->left = replace;
else
rb_parent(node)->right = replace;
}
else
// "node节点"是根节点,更新根节点。
root = replace;
// child是"取代节点"的右孩子,也是需要"调整的节点"。
// "取代节点"肯定不存在左孩子!因为它是一个后继节点。
child = replace->right;
parent = rb_parent(replace);
// 保存"取代节点"的颜色
color = rb_color(replace);
// "被删除节点"是"它的后继节点的父节点"
if (parent == node)
{
parent = replace;
}
else
{
// child不为空
if (child)
rb_set_parent(child, parent);
parent->left = child;
replace->right = node->right;
rb_set_parent(node->right, replace);
}
replace->parent = node->parent;
replace->color = node->color;
replace->left = node->left;
node->left->parent = replace;
if (color == BLACK)
removeFixUp(root, child, parent);
delete node;
return ;
}
if (node->left !=NULL)
child = node->left;
else
child = node->right;
parent = node->parent;
// 保存"取代节点"的颜色
color = node->color;
if (child)
child->parent = parent;
// "node节点"不是根节点
if (parent)
{
if (parent->left == node)
parent->left = child;
else
parent->right = child;
}
else
root = child;
if (color == BLACK)
removeFixUp(root, child, parent);
delete node;
}
/*
* 删除红黑树中键值为key的节点
*
* 参数说明:
* tree 红黑树的根结点
*/
template
void RBTree::remove(T key)
{
RBTNode *node;
// 查找key对应的节点(node),找到的话就删除该节点
if ((node = search(mRoot, key)) != NULL)
remove(mRoot, node);
}
/*
* 销毁红黑树
*/
template
void RBTree::destroy(RBTNode* &tree)
{
if (tree==NULL)
return ;
if (tree->left != NULL)
return destroy(tree->left);
if (tree->right != NULL)
return destroy(tree->right);
delete tree;
tree=NULL;
}
template
void RBTree::destroy()
{
destroy(mRoot);
}
/*
* 打印"二叉查找树"
*
* key -- 节点的键值
* direction -- 0,表示该节点是根节点;
* -1,表示该节点是它的父结点的左孩子;
* 1,表示该节点是它的父结点的右孩子。
*/
template
void RBTree::print(RBTNode* tree, T key, int direction)
{
if(tree != NULL)
{
if(direction==0) // tree是根节点
cout << setw(2) << tree->key << "(B) is root" << endl;
else // tree是分支节点
cout << setw(2) << tree->key << (rb_is_red(tree)?"(R)":"(B)") << " is " << setw(2) << key << "'s " << setw(12) << (direction==1?"right child" : "left child") << endl;
print(tree->left, tree->key, -1);
print(tree->right,tree->key, 1);
}
}
template
void RBTree::print()
{
if (mRoot != NULL)
print(mRoot, mRoot->key, 0);
}
#endif
//RBTree.cpp
/**
* C++ 语言: 二叉查找树
*
* @author skywang
* @date 2013/11/07
*/
#include
#include "RBTree.h"
using namespace std;
int main()
{
int a[]= {10, 40, 30, 60, 90, 70, 20, 50, 80};
int check_insert=0; // "插入"动作的检测开关(0,关闭;1,打开)
int check_remove=0; // "删除"动作的检测开关(0,关闭;1,打开)
int i;
int ilen = (sizeof(a)) / (sizeof(a[0])) ;
RBTree* tree=new RBTree();
cout << "== 原始数据: ";
for(i=0; iinsert(a[i]);
// 设置check_insert=1,测试"添加函数"
if(check_insert)
{
cout << "== 添加节点: " << a[i] << endl;
cout << "== 树的详细信息: " << endl;
tree->print();
cout << endl;
}
}
cout << "== 前序遍历: ";
tree->preOrder();
cout << "\n== 中序遍历: ";
tree->inOrder();
cout << "\n== 后序遍历: ";
tree->postOrder();
cout << endl;
cout << "== 最小值: " << tree->minimum() << endl;
cout << "== 最大值: " << tree->maximum() << endl;
cout << "== 树的详细信息: " << endl;
tree->print();
// 设置check_remove=1,测试"删除函数"
if(check_remove)
{
for(i=0; iremove(a[i]);
cout << "== 删除节点: " << a[i] << endl;
cout << "== 树的详细信息: " << endl;
tree->print();
cout << endl;
}
}
// 销毁红黑树
tree->destroy();
return 0;
}
线段树
线段树详解
https://www.cnblogs.com/xenny...
我自己在学这些数据结构以及算法的时候,网上的博客很多都是给出一个大致思想,然后就直接给代码了,可能是我智商太低,思维跳跃没有那么大,没法直接代码实现,而且有些学完之后也没有得到深层次的理解和运用,还是停留在只会使用模板的基础上。所以我希望我写的东西能让更多的人看明白,我会尽量写详细,也会写出我初学的时候哪些地方没有理解或者难以运用,又是怎样去熟练的使用这些东西的。可能还是不能让所有的人都读明白,但我尽量做的更好。
一、什么是线段树?
- 线段树是怎样的树形结构?
线段树是一种二叉搜索树,什么叫做二叉搜索树,首先满足二叉树,每个结点度小于等于二,即每个结点最多有两颗子树,何为搜索,我们要知道,线段树的每个结点都存储了一个区间,也可以理解成一个线段,而搜索,就是在这些线段上进行搜索操作得到你想要的答案。
- 线段树能够解决什么样的问题。
线段树的适用范围很广,可以在线维护修改以及查询区间上的最值,求和。更可以扩充到二维线段树(矩阵树)和三维线段树(空间树)。对于一维线段树来说,每次更新以及查询的时间复杂度为O(logN)。
- 线段树和其他RMQ算法的区别
常用的解决RMQ问题有ST算法,二者预处理时间都是O(NlogN),而且ST算法的单次查询操作是O(1),看起来比线段树好多了,但二者的区别在于线段树支持在线更新值,而ST算法不支持在线操作。
这里也存在一个误区,刚学线段树的时候就以为线段树和树状数组差不多,用来处理RMQ问题和求和问题,但其实线段树的功能远远不止这些,我们要熟练的理解线段这个概念才能更加深层次的理解线段树。
二、线段树的基本内容
现在请各位不要带着线段树只是为了解决区间问题的数据结构,事实上,是线段树多用于解决区间问题,并不是线段树只能解决区间问题,首先,我们得先明白几件事情。
每个结点存什么,结点下标是什么,如何建树。
下面我以一个简单的区间最大值来阐述上面的三个概念。
对于A[1:6] = {1,8,6,4,3,5}来说,线段树如上所示,红色代表每个结点存储的区间,蓝色代表该区间最值。
可以发现,每个叶子结点的值就是数组的值,每个非叶子结点的度都为二,且左右两个孩子分别存储父亲一半的区间。每个父亲的存储的值也就是两个孩子存储的值的最大值。
上面的每条结论应该都容易看出来。那么结点到底是如何存储区间的呢,以及如何快速找到非叶子结点的孩子以及非根节点的父亲呢,这里也就是理解线段树的重点以及难点所在,如同树状数组你理解了lowbit就能很快理解树状数组一样,线段树你只要理解了结点与结点之间的关系便能很快理解线段树的基本知识。
对于一个区间[l,r]来说,最重要的数据当然就是区间的左右端点l和r,但是大部分的情况我们并不会去存储这两个数值,而是通过递归的传参方式进行传递。这种方式用指针好实现,定义两个左右子树递归即可,但是指针表示过于繁琐,而且不方便各种操作,大部分的线段树都是使用数组进行表示,那这里怎么快速使用下标找到左右子树呢。
对于上述线段树,我们增加绿色数字为每个结点的下标
则每个结点下标如上所示,这里你可能会问,为什么最下一排的下标直接从9跳到了12,道理也很简单,中间其实是有两个空间的呀!!虽然没有使用,但是他已经开了两个空间,这也是为什么无优化的线段树建树需要22k(2k-1 < n < 2k)空间,一般会开到4n的空间防止RE。
仔细观察每个父亲和孩子下标的关系,有发现什么联系吗?不难发现,每个左子树的下标都是偶数,右子树的下标都是奇数且为左子树下标+1,而且不难发现以下规律
- l = fa*2 (左子树下标为父亲下标的两倍)
- r = fa*2+1(右子树下标为父亲下标的两倍+1)
具体证明也很简单,把线段树看成一个完全二叉树(空结点也当作使用)对于任意一个结点k来说,它所在此二叉树的log2(k) 层,则此层共有2log2(k)个结点,同样对于k的左子树那层来说有2log2(k)+1个结点,则结点k和左子树间隔了22log2(k)-k + 2(k-2log2(k))个结点,然后这就很简单就得到k+22log2(k)-k + 2(k-2log2(k)) = 2*k的关系了吧,右子树也就等于左子树结点+1。
是不是觉得其实很简单,而且因为左子树都是偶数,所以我们常用位运算来寻找左右子树
- k<<1(结点k的左子树下标)
- k<<1|1(结点k的右子树下标)
整理一下思绪,现在已经明白了数组如何存在线段树,结点间的关系,以及使用递归的方式建立线段树,那么具体如何建立线段树,我们来看代码,代码中不清楚的地方都有详细的注释说明。
const int maxn = 100005;
int a[maxn],t[maxn<<2]; //a为原来区间,t为线段树
void Pushup(int k){ //更新函数,这里是实现最大值 ,同理可以变成,最小值,区间和等
t[k] = max(t[k<<1],t[k<<1|1]);
}
//递归方式建树 build(1,1,n);
void build(int k,int l,int r){ //k为当前需要建立的结点,l为当前需要建立区间的左端点,r则为右端点
if(l == r) //左端点等于右端点,即为叶子节点,直接赋值即可
t[k] = a[l];
else{
int m = l + ((r-l)>>1); //m则为中间点,左儿子的结点区间为[l,m],右儿子的结点区间为[m+1,r]
build(k<<1,l,m); //递归构造左儿子结点
build(k<<1|1,m+1,r); //递归构造右儿子结点
Pushup(k); //更新父节点
}
}
现在再来看代码,是不是觉得清晰很多了,使用递归的方法建立线段树,确实清晰易懂,各位看到这里也请自己试着实现一下递归建树,若是哪里有卡点再来看一下代码找到哪里出了问题。那线段树有没有非递归的方式建树呢,答案是有,但是非递归的建树方式会使得线段树的查询等操作和递归建树方式完全不一样,由简至难,后面我们再说非递归方式的实现。
到现在你应该可以建立一颗线段树了,而且知道每个结点存储的区间和值,如果上述操作还不能实现或是有哪里想不明白,建议再翻回去看一看所讲的内容。不要急于看完,理解才更重要。
三、线段树的基本操作
基本操作有哪些,你应该也能想出来,在线的二叉搜索树,所拥有的操作当然有,更新和询问两种。
1.点更新
如何实现点更新,我们先不急看代码,还是对于上面那个线段树,假使我把a[3]+7,则更新后的线段树应该变成
更新了a[3]后,则每个包含此值的结点都需要更新,那么有多少个结点需要更新呢?根据二叉树的性质,不难发现是log(k)个结点,这也正是为什么每次更新的时间复杂度为O(logN),那应该如何实现呢,我们发现,无论你更新哪个叶子节点,最终都是会到根结点的,而把这个往上推的过程逆过来就是从根结点开始,找到左子树还是右子树包含需要更新的叶子节点,往下更新即可,所以我们还是可以使用递归的方法实现线段树的点更新
//递归方式更新 updata(p,v,1,n,1);
void updata(int p,int v,int l,int r,int k){ //p为下标,v为要加上的值,l,r为结点区间,k为结点下标
if(l == r) //左端点等于右端点,即为叶子结点,直接加上v即可
a[k] += v,t[k] += v; //原数组和线段树数组都得到更新
else{
int m = l + ((r-l)>>1); //m则为中间点,左儿子的结点区间为[l,m],右儿子的结点区间为[m+1,r]
if(p <= m) //如果需要更新的结点在左子树区间
updata(p,v,l,m,k<<1);
else //如果需要更新的结点在右子树区间
updata(p,v,m+1,r,k<<1|1);
Pushup(k); //更新父节点的值
}
}
看完代码是不是很清晰,这里也建议自己再次手动实现一遍理解递归的思路。
2.区间查询
说完了单点更新肯定就要来说区间查询了,我们知道线段树的每个结点存储的都是一段区间的信息 ,如果我们刚好要查询这个区间,那么则直接返回这个结点的信息即可,比如对于上面线段树,如果我直接查询[1,6]这个区间的最值,那么直接返回根节点信息返回13即可,但是一般我们不会凑巧刚好查询那些区间,比如现在我要查询[2,5]区间的最值,这时候该怎么办呢,我们来看看哪些区间是[2,5]的真子集,
一共有5个区间,而且我们可以发现[4,5]这个区间已经包含了两个子树的信息,所以我们需要查询的区间只有三个,分别是[2,2],[3,3],[4,5],到这里你能通过更新的思路想出来查询的思路吗? 我们还是从根节点开始往下递归,如果当前结点是要查询的区间的真子集,则返回这个结点的信息且不需要再往下递归了,这样从根节点往下递归,时间复杂度也是O(logN)。那么代码则为
//递归方式区间查询 query(L,R,1,n,1);
int query(int L,int R,int l,int r,int k){ //[L,R]即为要查询的区间,l,r为结点区间,k为结点下标
if(L <= l && r <= R) //如果当前结点的区间真包含于要查询的区间内,则返回结点信息且不需要往下递归
return t[k];
else{
int res = -INF; //返回值变量,根据具体线段树查询的什么而自定义
int mid = l + ((r-l)>>1); //m则为中间点,左儿子的结点区间为[l,m],右儿子的结点区间为[m+1,r]
if(L <= m) //如果左子树和需要查询的区间交集非空
res = max(res, query(L,R,l,m,k<<1));
if(R > m) //如果右子树和需要查询的区间交集非空,注意这里不是else if,因为查询区间可能同时和左右区间都有交集
res = max(res, query(L,R,m+1,r,k<<1|1));
return res; //返回当前结点得到的信息
}
}
如果你能理解建树和更新的过程,那么这里的区间查询也不会太难理解。还是建议再次手动实现。
3.区间更新
树状数组中的区间更新我们用了差分的思想,而线段树的区间更新相对于树状数组就稍微复杂一点,这里我们引进了一个新东西,Lazy_tag,字面意思就是懒惰标记的意思,实际上它的功能也就是偷懒= =,因为对于一个区间[L,R]来说,我们可能每次都更新区间中的没个值,那样的话更新的复杂度将会是O(NlogN),这太高了,所以引进了Lazy_tag,这个标记一般用于处理线段树的区间更新。
线段树在进行区间更新的时候,为了提高更新的效率,所以每次更新只更新到更新区间完全覆盖线段树结点区间为止,这样就会导致被更新结点的子孙结点的区间得不到需要更新的信息,所以在被更新结点上打上一个标记,称为lazy-tag,等到下次访问这个结点的子结点时再将这个标记传递给子结点,所以也可以叫延迟标记。
也就是说递归更新的过程,更新到结点区间为需要更新的区间的真子集不再往下更新,下次若是遇到需要用这下面的结点的信息,再去更新这些结点,所以这样的话使得区间更新的操作和区间查询类似,复杂度为O(logN)。
void Pushdown(int k){ //更新子树的lazy值,这里是RMQ的函数,要实现区间和等则需要修改函数内容
if(lazy[k]){ //如果有lazy标记
lazy[k<<1] += lazy[k]; //更新左子树的lazy值
lazy[k<<1|1] += lazy[k]; //更新右子树的lazy值
t[k<<1] += lazy[k]; //左子树的最值加上lazy值
t[k<<1|1] += lazy[k]; //右子树的最值加上lazy值
lazy[k] = 0; //lazy值归0
}
}
//递归更新区间 updata(L,R,v,1,n,1);
void updata(int L,int R,int v,int l,int r,int k){ //[L,R]即为要更新的区间,l,r为结点区间,k为结点下标
if(L <= l && r <= R){ //如果当前结点的区间真包含于要更新的区间内
lazy[k] += v; //懒惰标记
t[k] += v; //最大值加上v之后,此区间的最大值也肯定是加v
}
else{
Pushdown(k); //重难点,查询lazy标记,更新子树
int m = l + ((r-l)>>1);
if(L <= m) //如果左子树和需要更新的区间交集非空
update(L,R,v,l,m,k<<1);
if(m < R) //如果右子树和需要更新的区间交集非空
update(L,R,v,m+1,r,k<<1|1);
Pushup(k); //更新父节点
}
}
注意看Pushdown这个函数,也就是当需要查询某个结点的子树时,需要用到这个函数,函数功能就是更新子树的lazy值,可以理解为平时先把事情放着,等到哪天要检查的时候,就临时再去做,而且做也不是一次性做完,检查哪一部分它就只做这一部分。是不是感受到了什么是Lazy_tag,实至名归= =。
值得注意的是,使用了Lazy_tag后,我们再进行区间查询也需要改变。区间查询的代码则变为
//递归方式区间查询 query(L,R,1,n,1);
int query(int L,int R,int l,int r,int k){ //[L,R]即为要查询的区间,l,r为结点区间,k为结点下标
if(L <= l && r <= R) //如果当前结点的区间真包含于要查询的区间内,则返回结点信息且不需要往下递归
return t[k];
else{
Pushdown(k); /**每次都需要更新子树的Lazy标记*/
int res = -INF; //返回值变量,根据具体线段树查询的什么而自定义
int mid = l + ((r-l)>>1); //m则为中间点,左儿子的结点区间为[l,m],右儿子的结点区间为[m+1,r]
if(L <= m) //如果左子树和需要查询的区间交集非空
res = max(res, query(L,R,l,m,k<<1));
if(R > m) //如果右子树和需要查询的区间交集非空,注意这里不是else if,因为查询区间可能同时和左右区间都有交集
res = max(res, query(L,R,m+1,r,k<<1|1));
return res; //返回当前结点得到的信息
}
}
其实变动也不大,就是多了一个临时更新子树的值的过程。
四、线段树的其他操作
如果你明白了上述线段树处理区间最值的所有操作,那么转变成求最小值以及区间和问题应该也能很快解决,请手动再实现一下查询区间最小值的线段树和查询区间和的线段树。
区间和线段树等代码不再给出,自行实现,若不能实现可以去网上搜索模板对比自己为何不能实现。这里便不再浪费篇幅讲述。
这里我便是想说一下线段树还能处理的问题以及一些具体问题讲解。上述我们只是再讲线段树处理裸区间问题,但是大部分问题不会是让你直接更新查询,而是否真正理解线段树便在于思维是否能从区间跳到线段。
区间只是一个线段的一小部分,还有一些非区间问题也可以演变成一段一段的线段,然后再通过线段树进行各种操作。下面针对几道例题讲解一下线段树的其他具体用法。
下面三道题讲解并非自己所写,而是摘取了另一篇线段树的博客,特此声明,原博客地址:https://blog.csdn.net/whereis...
1.区间染色
给定一个长度为n(n <= 100000)的木板,支持两种操作:
1、P a b c 将[a, b]区间段染色成c;
2、Q a b 询问[a, b]区间内有多少种颜色;
保证染色的颜色数少于30种。
对比区间求和,不同点在于区间求和的更新是对区间和进行累加;而这类染色问题则是对区间的值进行替换(或者叫覆盖),有一个比较特殊的条件是颜色数目小于30。
我们是不是要将30种颜色的有无与否都存在线段树的结点上呢?答案是肯定的,但是这样一来每个结点都要存储30个bool值,空间太浪费,而且在计算合并操作的时候有一步30个元素的遍历,大大降低效率。然而30个bool值正好可以压缩在一个int32中,利用二进制压缩可以用一个32位的整型完美的存储30种颜色的有无情况。
因为任何一个整数都可以分解成二进制整数,二进制整数的每一位要么是0,要么是1。二进制整数的第i位是1表示存在第i种颜色;反之不存在。
数据域需要存一个颜色种类的位或和colorBit,一个颜色的lazy标记表示这个结点被完全染成了lazy,基本操作的几个函数和区间求和非常像,这里就不出示代码了。
和区间求和不同的是回溯统计的时候,对于两个子结点的数据域不再是加和,而是位或和。
2.区间第K大
给定n个数,每次询问问[l,r]区间内的第K大数,这个问题有很多方法,但是用线段树应该如何解决呢。
利用了线段树划分区间的思想,线段树的每个结点存的不只是区间端点,而是这个区间内所有的数,并且是按照递增顺序有序排列的,建树过程是一个归并排序的过程,从叶子结点自底向上进行归并,对于一个长度为6的数组[4, 3, 2, 1, 5, 6],建立线段树如图所示。
从图中可以看出,线段树的任何一个结点存储了对应区间的数,并且进行有序排列,所以根结点存储的一定是一个长度为数组总长的有序数组,叶子结点存储的递增序列为原数组元素。
每次询问,我们将给定区间拆分成一个个线段树上的子区间,然后二分枚举答案T,再利用二分查找统计这些子区间中大于等于T的数的个数,从而确定T是否是第K大的。
对于区间K大数的问题,还有很多数据结构都能解决,这里仅作简单介绍。
3.矩阵面积并
对于给定的n(n<=100000)个平行于XY轴的矩形,求他们的面积并。
这是一个二维的问题,如果我告诉你这道题使用线段树解决,你该如何入手呢,首先线段树是一维的,所以我们需要化二维为一维,所以我们可以使用x的坐标或者y的坐标建立线段树,另一坐标用来进行枚举操作。
我们用x的坐标来建树的化,那么我们把矩阵平行于x轴的线段舍去,则变成了
每个矩形都剩下两条边,定义x坐标较小的为入边(值为+1),较大为出边(值为-1),然后用x的升序,记第i条线段的x坐标即为X[i]
接下来将所有矩形端点的y坐标进行重映射(也可以叫离散化),原因是坐标有可能很大而且不一定是整数,将原坐标映射成小范围的整数可以作为数组下标,更方便计算,映射可以将所有y坐标进行排序去重,然后二分查找确定映射后的值,离散化的具体步骤下文会详细讲解。如图所示,蓝色数字表示的是离散后的坐标,即1、2、3、4分别对应原先的5、10、23、25(需支持正查和反查)。假设离散后的y方向的坐标个数为m,则y方向被分割成m-1个独立单元,下文称这些独立单元为“单位线段”,分别记为<1-2>、<2-3>、<3-4>。
以x坐标递增的方式枚举每条垂直线段,y方向用一个长度为m-1的数组来维护“单位线段”的权值,如图所示,展示了每条线段按x递增方式插入之后每个“单位线段”的权值。
当枚举到第i条线段时,检查所有“单位线段”的权值,所有权值大于零的“单位线段”的实际长度之和(离散化前的长度)被称为“合法长度”,记为L,那么(X[i] - X[i-1]) * L,就是第i条线段和第i-1条线段之间的矩形面积和,计算完第i条垂直线段后将它插入,所谓"插入"就是利用该线段的权值更新该线段对应的“单位线段”的权值和(这里的更新就是累加)。
如图四-4-6所示:红色、黄色、蓝色三个矩形分别是3对相邻线段间的矩形面积和,其中红色部分的y方向由<1-2>、<2-3>两个“单位线段”组成,黄色部分的y方向由<1-2>、<2-3>、<3-4>三个“单位线段”组成,蓝色部分的y方向由<2-3>、<3-4>两个“单位线段”组成。特殊的,在计算蓝色部分的时候,<1-2>部分的权值由于第3条线段的插入(第3条线段权值为-1)而变为零,所以不能计入“合法长度”。
以上所有相邻线段之间的面积和就是最后要求的矩形面积并。
优化自然就是用线段树了,之前提到了降维的思想,x方向我们继续采用枚举,而y方向的“单位线段”则可以采用线段树来维护,
然后通过一个扫描线来求扫描线覆盖的y的长度。线段的扫描按照x的大小从小到大扫描,求出当前扫描线覆盖的矩阵竖线的长度,然后乘以下条线段的跨度,则为这个区域矩阵覆盖的面积,具体关于扫描线的操作这里不再阐述。这里只讲明白如何建树。
五、线段树的一些重难点以及技巧
1.离散化
离散化常用于二维状态在一维线段树建树,所谓离散化就是将无限的个体映射到有限个体中,提高算法效率,而且支持正查和反查(从开始遍历和从末尾遍历),可用Hash等实现。
2.Lazy_tag
这个标记就是用于线段树的区间更新,上面已经提到,便不再累赘,但是区间更新并不局限于使用Lazy_tag,还有一种不使用Lazy_tag的区间更新方法,会在提高篇中讲到。
3.空间优化
父节点k,左儿子k<<1,右儿子k<<1|1,则需要n<<2的空间,但我们知道并不是所有的叶子节点都占用到了2n+1 —— 4n的范围,造成了大量空间浪费。这时候就要考虑离散化,压缩空间。或者使用dfs序作为结点下标,父亲k,左儿子k+1,右儿子k+左儿子区间长度*2,具体实现不再累赘,可自行通过修改左右儿子的下标推出。
4.多维推广
例如矩阵树,空间树,这些便是线段树的拓展,比如要在两种不同的参数找到最适变量,例如对于一个人的身高和体重,找到一定范围内且年龄最小的人,就可以用到多维推广了。
5.可持久化
主席树。以后讲= =
6.非递归形式
前面提到过这个概念,非递归形式的某些操作会快于递归形式,以后将会专门将非递归形式。
7.子树收缩
就是子树继承的逆过程,继承是为了得到父节点信息,而收缩则是在回溯时候,如果两棵子树拥有相同数据的时候在将数据传递给父结点,子树的数据清空,这样下次在访问的时候就可以减少访问的结点数。
六、相关例题
- codevs 1080 (单点修改+区间查询)
- codevs 1081 (区间修改+单点查询)
- codevs 1082 (区间修改+区间查询)
- codevs 3981 (区间最大子段和)
- Bzoj 3813 (区间内某个值是否出现过)
- Luogu P2894 (区间连续一段空的长度)
- codevs 2000 (区间最长上升子序列)
- codevs 3044 (矩阵面积求并)
- Hdu 1698 (区间染色+单次统计)
- Poj 2777 (区间染色+批量统计)
- Hdu 4419 (多色矩形面积并)
- Poj 2761 (区间第K大)
- Hdu 2305 (最值维护)
主席树(可持久化线段树)
题目背景
这是个非常经典的主席树入门题——静态区间第 kk 小。
数据已经过加强,请使用主席树。同时请注意常数优化。
题目描述
如题,给定 nn 个整数构成的序列 aa,将对于指定的闭区间 l, r 查询其区间内的第 kk 小值。
输入格式
第一行包含两个整数,分别表示序列的长度 nn 和查询的个数 mm。
第二行包含 nn 个整数,第 ii 个整数表示序列的第 ii 个元素 a_iai。
接下来 mm 行每行包含三个整数 l, r, kl,r,k , 表示查询区间 l, r 内的第 kk 小值。
输出格式
对于每次询问,输出一行一个整数表示答案。
输入输出样例
Input1:
5 5
25957 6405 15770 26287 26465
2 2 1
3 4 1
4 5 1
1 2 2
4 4 1
Input2:
6405
15770
26287
25957
26287
说明/提示
样例 1 解释
n=5n=5,数列长度为 55,数列从第一项开始依次为{25957, 6405, 15770, 26287, 26465}{25957,6405,15770,26287,26465}。
- 第一次查询为 2, 2 区间内的第一小值,即为 64056405。
- 第二次查询为 3, 4 区间内的第一小值,即为 1577015770。
- 第三次查询为 4, 5 区间内的第一小值,即为 2628726287。
- 第四次查询为 1, 2 区间内的第二小值,即为 2595725957。
- 第五次查询为 4, 4 区间内的第一小值,即为 2628726287。
数据规模与约定
- 对于20%的数据,满足 1 <= n,m <= 10;
- 对于50%的数据,满足 1 <=n,m <= 10^3;
- 对于80%的数据,满足 1 <= n,m <= 10^5;
- 对于100%的数据, 满足 1 <= n,m <= 2 * 10^5, 1 <=l <= r <= n, 1 <= k <= r-l + 1 , ai的绝对值<= 10^9
解决问题
暴力法
显而易见,最暴力的办法就是区间排序然后输出排序后第kk个数。最坏情况的时间复杂度是O(nmlgn)O(nmlgn),不超时才怪。
主席树
https://blog.csdn.net/riba253...
于是针对这个问题,新的数据结构诞生了,也就是主席树。
主席树本名可持久化线段树,也就是说,主席树是基于线段树发展而来的一种数据结构。其前缀”可持久化”意在给线段树增加一些历史点来维护历史数据,使得我们能在较短时间内查询历史数据,图示如下。
图中的橙色节点为历史节点,其右边多出来的节点是新节点(修改节点)。
下面我们来讲怎么构建这个数据结构。
主席树对点的修改
不同于普通线段树的是主席树的左右子树节点编号并不能够用计算得到,所以我们需要记录下来,但是对应的区间还是没问题的
//节点o表示区间[l,r],修改点为p,修改值根据题意设定(此处我们先不谈题目,只谈数据结构)
int modify(int o, int l, int r, int p)
{
int oo = ++node_cnt;
lc[oo] = lc[o]; rc[oo] = rc[o]; sum[oo] = sum[o] + 1;//新节点,这里是根据模板题来的
if(l == r)//递归底层返回新节点编号,修改父节点的儿子指向
{
//sum[oo] = t;如果题目要求sum是加t的再这样弄,然后上面的+1就去掉
return oo;
}
int mid = (l + r) >> 1;
if(p <= mid) lc[oo] = modify(lc[oo], l, mid);
else rc[oo] = modify(rc[oo], mid+1, r);
//sum[oo] = sum[lc[oo]] + sum[rc[oo]];在该题中,不需要这样做,但是很多情况下是要这样更新的
return oo;
}
至于主席树的区间修改,其实也不难,但是复杂度有点高,简单点的题目一般只有点修改,有时候区间修改可以转化为点修改(比如NOIP2012借教室,有区间修改的解法也有点修改的解法)。
主席树的询问(历史区间和)
int ql, qr;//查询区间[l,r]
int query(int o, int l, int r)//节点o代表区间[l,r]
{
int ans = 0, mid = ((l + r) >> 1);
if(!o) return 0;//不存在的子树
if(ql <= l && r <= qr) return sum[o];//区间包含返回区间值
//都是线段树标准操作,只不过是左右子树多了一个记录而已
if(ql <= mid) ans += query(lc[o], l, mid);
if(qr > mid) ans += query(rc[o], mid+1, r);
return ans;
//点操作就不用说了
}
模板题解答
模板题[1,r]的情况
由题意知道我们肯定要对区间进行排序,但是我们的排序不是每次询问才排序,是初始化就排序离散化——针对数字较大但数据较小的情况(具体见方法)。排序离散化完毕后,以离散化数组建主席树,设ii属于区间1,n,对原数组的1,i区间的数做统计(例如下图,区间中按离散化数组顺序统计11的个数、22的个数、33的个数、44的个数、88的个数、99的个数),有序地插入节点到离散化数组的主席树中,记录好原数组每个节点对应的线段树起点,针对样例有几个示意图。注意,这里的橙色节点是新节点,与之前出现的那个图不一样。
- 1,1的情况
- 1,4的情况
情况以此类推。
我们按照上面的做法构建的主席树是为了方便我们查找第kk小值。因为我们是以离散数组构建的主席树,那么从根节点出发,左子树部分的数必定不大于右子树部分的数。于是就可以将左儿子的节点个数xx与kk做比较,若k≤xk≤x,则第kk小值一定在左子树里面,若x≤kx≤k,则第kk小值一定在右子树里面,然后递归往下走,缩小范围。值得注意的是,前者递归时,kk直接传下去即可,后者递归时,需要将kk减去左子树的数的个数再传递这个kk值。
例如我们查找1,4中第22小的值,图示如下,绿色节点为该值存在的区间位置。
需要注意的是,第二个绿色节点才是绿色根节点的左子树,因为左子树表示的区间是靠前的那一半。
方法总结如下:
- 将原始数组复制一份,然后排序好,然后去掉多余的数,即将数据离散化。推荐使用C++的STL中的
unique
函数; - 以离散化数组为基础,建一个全00的线段树,称作基础主席树;
- 对原数据中每一个1,i区间统计,有序地插入新节点(题目中ii每增加11就会多一个数,仅需对主席树对应的节点增加11即可);
对于查询1,r中第kk小值的操作,找到1,r对应的根节点,我们按照线段树的方法操作即可(这个根节点及其子孙构成的必定是一颗线段树)。
模板问题的解决
现在我们真正来解决区间询问l,r的问题。
构建主席树的方法是没有问题的,问题正在于区间询问怎么写。其实,解决方案就是将主席树1,r减去主席树1,l−1就行了。其实这个原因并不难想,首先看到主席树的底层,全部是对数的统计。当主席树1,r减去主席树1,l−1时,统计也跟着减了,也就是说,现在统计记录的是l,r区间。
而我们不需要单独减,只需要边递归查询边减,具体见查询部分代码。
//初始的u和v分别代表的是点l-1和点r,l和r分别表示线段树点代表的区间,初始的k如题
int query(int u, int v, int l, int r, int k)
{
int ans, mid = ((l + r) >> 1), x = sum[lc[v]] - sum[lc[u]];
//因为主席树是区间统计好了的,只要减一下即可,无需递归到叶子再处理
if(l == r)//找到目标位置
return l;
if(x >= k) ans = query(lc[u], lc[v], l, mid, k);
else ans = query(rc[u], rc[v], mid+1, r, k-x);//右子树记得改变k的值
return ans;
}
模板问题的Code
#include
#include
#define M 200010
using namespace std;
int node_cnt, n, m;
int sum[M<<5], rt[M], lc[M<<5], rc[M<<5];//线段树相关
int a[M], b[M];//原序列和离散序列
int p;//修改点
void build(int &t, int l, int r)
{
t = ++node_cnt;
if(l == r)
return;
int mid = (l + r) >> 1;
build(lc[t], l, mid);
build(rc[t], mid+1, r);
}
int modify(int o, int l, int r)
{
int oo = ++node_cnt;
lc[oo] = lc[o]; rc[oo] = rc[o]; sum[oo] = sum[o] + 1;
if(l == r)
return oo;
int mid = (l + r) >> 1;
if(p <= mid) lc[oo] = modify(lc[oo], l, mid);
else rc[oo] = modify(rc[oo], mid+1, r);
return oo;
}
int query(int u, int v, int l, int r, int k)
{
int ans, mid = ((l + r) >> 1), x = sum[lc[v]] - sum[lc[u]];
if(l == r)
return l;
if(x >= k) ans = query(lc[u], lc[v], l, mid, k);
else ans = query(rc[u], rc[v], mid+1, r, k-x);
return ans;
}
int main()
{
int l, r, k, q, ans;
scanf("%d%d", &n, &m);
for(register int i = 1; i <= n; i += 1)
scanf("%d", &a[i]), b[i] = a[i];
sort(b+1, b+n+1);
q = unique(b+1, b+n+1) - b - 1;
build(rt[0], 1, q);
for(register int i = 1; i <= n; i += 1)
{
p = lower_bound(b+1, b+q+1, a[i])-b;//可以视为查找最小下标的匹配值,核心算法是二分查找
rt[i] = modify(rt[i-1], 1, q);
}
while(m--)
{
scanf("%d%d%d", &l, &r, &k);
ans = query(rt[l-1], rt[r], 1, q, k);
printf("%d\n", b[ans]);
}
return 0;
}
复杂度分析
题目一开始的离散化复杂度为O(nlgn)O(nlgn),构建基础主席树复杂度为O(nlgn)O(nlgn),统计并插入的复杂度是O(nlgn+nlgn)=O(nlgn)O(nlgn+nlgn)=O(nlgn),询问的复杂度是O(mlgn)O(mlgn)。复杂度总和就是O((m+n)lgn)O((m+n)lgn)。
前缀树和后缀树
前缀树(字典树)
https://blog.csdn.net/weixin_...
什么是Trie树
Trie树,即前缀树,又称单词查找树,字典树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
Trie树的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。 它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
它有3个基本性质:
根节点不包含字符,除根节点外每一个节点都只包含一个字符。
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
每个节点的所有子节点包含的字符都不相同。
树的构建与查询
根据一个例子来描述trie树的构建:
题目:给定100000个长度不超过10的单词,判断每一个单词是否出现过,如果出现过,给出第一次出现的位置。
思路:如果对每一个单词都遍历整个单词序列,那么时间复杂度就是O(n2)O(n^2)O(n
2
),单词序列的长度是100000,显然这样的时间复杂度代价太高了。但是考虑100000个单词肯定有一些字符串的重复,trie树,即前缀树来解决这个问题最恰当不过了。一个前缀树的形式是什么样的呢?
假设有abc,abd,bcd,efg,hii,那么我们构建如下一个树结构用于表示这些单词:
但是构造一个前缀树就是为了使遍历那些有相同的前缀的单词时更快速,目前这个多叉树上从根节点到叶节点每一条路径都是一个单词,如果我们增加了单词b,abcd两个单词呢,还是这样一条路走到黑,我们无法区分字母短的单词是否已经存储过,为了解决这个问题,我们可以在节点上记录信息,比如如果到达该字母后单词结尾,我们就在时候的节点上记录end+1,这样就可以知道有几个以该前缀结尾的单词,修改之后结构如下:
这是我们可以很明显看出来如最左边一条路径上,有一个单词是abc,一个单词是abcd。根据新添加的信息,我们可以知道所有字符串中有多少个该字符串,如果现在有另外一个问题,我想知道所有字符串中有多少个以该字符串作为前缀,那是不是得遍历该条路径后面的节点,将所有的end加起来。为了简单,我们仍然可以在节点中多存入一个信息,每个节点被划过了多少次,也就是在建立多叉树的时候就记录了以所有字符串中有多少个以某字符串作为前缀,划过的次数就是这个值。调整后结构如下:注:在C++实现部分,图中所有的S都加1,单词本身也是以他自己作为前缀的。
回到最初的题目,这样的结构可以根据节点的信息得到一个单词时候出现过(End>0),但是有一个要求我们没有满足,那就是如果存在这个单词我们如何返回这个单词在原来列表中的位置呢?根据前面介绍的多叉树结构的介绍,我想这个问题也很容易解决,节点中我们再多存入一些信息就可以,对于非结尾字符End=0,不需要存储其他的信息,对于结尾字符End>0,此时再存入信息,标注该单词在100000个单词词表中的位置,构建查询多叉树的时候就可以直接返回这个位置信息了。
Trieste树的应用
节选自此文:[海量数据处理面试题集锦与Bit-map详解]
(https://blog.csdn.net/v_july_...
除了本文引言处所述的问题能应用Trie树解决之外,Trie树还能解决下述问题
3、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
9、1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。请怎么设计和实现?
10、 一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
13、寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
(1) 请描述你解决这个问题的思路;
(2) 请给出主要的处理流程,算法,以及算法的复杂度。
C++实现
解决有关字符串的一些问题:
一个字符串类型的数组arr1,另一个字符串类型的数组arr2。
arr2中有哪些字符串,是arr1中出现的?请打印
arr2中有哪些字符串,是作为arr1中某个字符串前缀出现的?请打印
arr2中有哪些字符串,是作为arr1中某个字符串前缀出现的?请打印arr2中出现次数最大的前缀。
#include
#include
#include
using namespace std;
const int MaxBranchNum = 26;//可以扩展
class TrieNode{
public:
string word;
int path; //该字符被划过多少次,用以统计以该字符串作为前缀的字符串的个数
int End; //以该字符结尾的字符串
TrieNode* nexts[MaxBranchNum];
TrieNode()
{
word = "";
path = 0;
End = 0;
memset(nexts,NULL,sizeof(TrieNode*) * MaxBranchNum);
}
};
class TrieTree{
private:
TrieNode *root;
public:
TrieTree();
~TrieTree();
//插入字符串str
void insert(string str);
//查询字符串str是否出现过,并返回作为前缀几次
int search(string str);
//删除字符串str
void Delete(string str);
void destory(TrieNode* root);
//打印树中的所有节点
void printAll();
//打印以str作为前缀的单词
void printPre(string str);
//按照字典顺序输出以root为根的所有单词
void Print(TrieNode* root);
//返回以str为前缀的单词的个数
int prefixNumbers(string str);
};
TrieTree::TrieTree()
{
root = new TrieNode();
}
TrieTree::~TrieTree()
{
destory(root);
}
void TrieTree::destory(TrieNode* root)
{
if(root == nullptr)
return ;
for(int i=0;inexts[i]);
}
delete root;
root = nullptr;
}
void TrieTree::insert(string str)
{
if(str == "")
return ;
char buf[str.size()];
strcpy(buf, str.c_str());
TrieNode* node = root;
int index = 0;
for(int i=0; inexts[index] == nullptr)
{
node->nexts[index] = new TrieNode();
}
node = node->nexts[index];
node->path++;//有一条路径划过这个节点
}
node->End++;
node->word = str;
}
int TrieTree::search(string str)
{
if(str == "")
return 0;
char buf[str.size()];
strcpy(buf, str.c_str());
TrieNode* node = root;
int index = 0;
for(int i=0;inexts[index] == nullptr)
{
return 0;
}
node = node->nexts[index];
}
if(node != nullptr)
{
return node->End;
}else
{
return 0;
}
}
void TrieTree::Delete(string str)
{
if(str == "")
return ;
char buf[str.size()];
strcpy(buf, str.c_str());
TrieNode* node = root;
TrieNode* tmp;
int index = 0;
for(int i = 0 ; inexts[index];
if(--node->nexts[index]->path == 0)
{
delete node->nexts[index];
}
node = tmp;
}
node->End--;
}
int TrieTree::prefixNumbers(string str)
{
if(str == "")
return 0;
char buf[str.size()];
strcpy(buf, str.c_str());
TrieNode* node = root;
int index = 0;
for(int i=0;inexts[index] == nullptr)
{
return 0;
}
node = node->nexts[index];
}
return node->path;
}
void TrieTree::printPre(string str)
{
if(str == "")
return ;
char buf[str.size()];
strcpy(buf, str.c_str());
TrieNode* node = root;
int index = 0;
for(int i=0;inexts[index] == nullptr)
{
return ;
}
node = node->nexts[index];
}
Print(node);
}
void TrieTree::Print(TrieNode* node)
{
if(node == nullptr)
return ;
if(node->word != "")
{
cout<word<<" "<path<nexts[i]);
}
}
void TrieTree::printAll()
{
Print(root);
}
int main()
{
cout << "Hello world!" << endl;
TrieTree trie;
string str = "li";
cout<
后缀树
定义
后缀树(Suffix tree)是一种数据结构,能快速解决很多关于字符串的问题。后缀树的概念最早由Weiner 于1973年提出,既而由McCreight 在1976年和Ukkonen在1992年和1995年加以改进完善。
后缀,顾名思义,甚至通俗点来说,就是所谓后缀就是后面尾巴的意思。比如说给定一长度为n的字符串S=S1S2..Si..Sn,和整数i,1 <= i <= n,子串SiSi+1...Sn便都是字符串S的后缀。
以字符串S=XMADAMYX为例,它的长度为8,所以S[1..8], S[2..8], ... , S[8..8]都算S的后缀,我们一般还把空字串也算成后缀。这样,我们一共有如下后缀。对于后缀S[i..n],我们说这项后缀起始于i。
S[1..8], XMADAMYX, 也就是字符串本身,起始位置为1
S[2..8], MADAMYX,起始位置为2
S[3..8], ADAMYX,起始位置为3
S[4..8], DAMYX,起始位置为4
S[5..8], AMYX,起始位置为5
S[6..8], MYX,起始位置为6
S[7..8], YX,起始位置为7
S[8..8], X,起始位置为8
空字串,记为$。
而后缀树,就是包含一则字符串所有后缀的压缩Trie。把上面的后缀加入Trie后,我们得到下面的结构:
仔细观察上图,我们可以看到不少值得压缩的地方。比如蓝框标注的分支都是独苗,没有必要用单独的节点同边表示。如果我们允许任意一条边里包含多个字 母,就可以把这种没有分叉的路径压缩到一条边。另外每条边已经包含了足够的后缀信息,我们就不用再给节点标注字符串信息了。我们只需要在叶节点上标注上每项后缀的起始位置。于是我们得到下图:
这样的结构丢失了某些后缀。比如后缀X在上图中消失了,因为它正好是字符串XMADAMYX的前缀。为了避免这种情况,我们也规定每项后缀不能是其它后缀的前缀。要解决这个问题其实挺简单,在待处理的子串后加一个空字串就行了。例如我们处理XMADAMYX前,先把XMADAMYX变为 XMADAMYX$,于是就得到suffix tree--后缀树了,如下图所示:
后缀树与回文问题的关联
最低共有祖先,LCA(Lowest Common Ancestor),也就是任意两节点(多个也行)最长的共有前缀。比如下图中,节点7同节点1的共同祖先是节点5与节点10,但最低共同祖先是5。 查找LCA的算法是O(1)的复杂度,当然,代价是需要对后缀树做复杂度为O(n)的预处理。
广义后缀树(Generalized Suffix Tree)。传统的后缀树处理一坨单词的所有后缀。广义后缀树存储任意多个单词的所有后缀。例如下图是单词XMADAMYX与XYMADAMX的广义后缀 树。注意我们需要区分不同单词的后缀,所以叶节点用不同的特殊符号与后缀位置配对。
最长回文问题
有了上面的概念,本文引言中提出的查找最长回文问题就相对简单了。咱们来回顾下引言中提出的回文问题的具体描述:找出给定字符串里的最长回文。例如输入XMADAMYX,则输出MADAM。
思维的突破点在于考察回文的半径,而不是回文本身。所谓半径,就是回文对折后的字串。比如回文MADAM 的半径为MAD,半径长度为3,半径的中心是字母D。显然,最长回文必有最长半径,且两条半径相等。还是以MADAM为例,以D为中心往左,我们得到半径 DAM;以D为中心向右,我们得到半径DAM。二者肯定相等。因为MADAM已经是单词XMADAMYX里的最长回文,我们可以肯定从D往左数的字串 DAMX与从D往右数的子串DAMYX共享最长前缀DAM。而这,正是解决回文问题的关键。现在我们有后缀树,怎么把从D向左数的字串DAMX变成后缀 呢?
到这个地步,答案应该明显:把单词XMADAMYX翻转(XMADAMYX=>XYMADAMX,DAMX就变成后缀了)就行了。于是我们把寻找回文的问题转换成了寻找两坨后缀的LCA的问题。当然,我们还需要知道 到底查询那些后缀间的LCA。很简单,给定字符串S,如果最长回文的中心在i,那从位置i向右数的后缀刚好是S(i),而向左数的字符串刚好是翻转S后得到的字符串S‘的后缀S'(n-i+1)。这里的n是字符串S的长度。
可能上面的阐述还不够直观,我再细细说明下:
1、首先,还记得本第二部分开头关于后缀树的定义么: “先说说后缀的定义,顾名思义,甚至通俗点来说,就是所谓后缀就是后面尾巴的意思。比如说给定一长度为n的字符串S=S1S2..Si..Sn,和整数i,1 <= i <= n,子串SiSi+1...Sn便都是字符串S的后缀。”
以字符串S=XMADAMYX为例,它的长度为8,所以S[1..8], S[2..8], ... , S[8..8]都算S的后缀,我们一般还把空字串也算成后缀。这样,我们一共有如下后缀。对于后缀S[i..n],我们说这项后缀起始于i。
S[1..8], XMADAMYX, 也就是字符串本身,起始位置为1
S[2..8], MADAMYX,起始位置为2
S[3..8], ADAMYX,起始位置为3
S[4..8], DAMYX,起始位置为4
S[5..8], AMYX,起始位置为5
S[6..8], MYX,起始位置为6
S[7..8], YX,起始位置为7
S[8..8], X,起始位置为8
空字串,记为$。
2、对单词XMADAMYX而言,回文中心为D,那么D向右的后缀DAMYX假设是S(i)(当N=8,i从1开始计数,i=4时,便是S(4..8));而对于翻转后的单词XYMADAMX而言,回文中心D向右对应的后缀为DAMX,也就是S'(N-i+1)((N=8,i=4,便是S‘(5..8)) 。此刻已经可以得出,它们共享最长前缀,即LCA(DAMYX,DAMX)=DAM。有了这套直观解释,算法自然呼之欲出:
预处理后缀树,使得查询LCA的复杂度为O(1)。这步的开销是O(N),N是单词S的长度 ;
对单词的每一位置i(也就是从0到N-1),获取LCA(S(i), S‘(N-i+1)) 以及LCA(S(i+1), S’(n-i+1))。查找两次的原因是我们需要考虑奇数回文和偶数回文的情况。这步要考察每坨i,所以复杂度是O(N) ;
找到最大的LCA,我们也就得到了回文的中心i以及回文的半径长度,自然也就得到了最长回文。总的复杂度O(n)。
用上图做例子,i为4时,LCA(4$, 5#)为DAM,正好是最长半径。当然,这只是直观的叙述。
上面大致描述了后缀树的基本思路。要想写出实用代码,至少还得知道下面的知识:
创建后缀树的O(n)算法。此算法有很多种,无论Peter Weiner的73年年度最佳算法,还是Edward McCreight1976的改进算法,还是1995年E. Ukkonen大幅简化的算法(本文第4部分将重点阐述这种方法),还是Juha Kärkkäinen 和 Peter Sanders2003年进一步简化的线性算法,都是O(n)的时间复杂度。至于实际中具体选择哪一种算法,可依实际情况而定。
实现后缀树用的数据结构。比如常用的子结点加兄弟节点列表,Directed 优化后缀树空间的办法。比如不存储子串,而存储读取子串必需的位置。以及Directed Acyclic Word Graph,常缩写为黑哥哥们挂在嘴边的DAWG。
后缀树的应用
后缀树的用途,总结起来大概有如下几种
1.查找字符串o是否在字符串S中。
方案:用S构造后缀树,按在trie中搜索字串的方法搜索o即可。
原理:若o在S中,则o必然是S的某个后缀的前缀。
例如S: leconte,查找o: con是否在S中,则o(con)必然是S(leconte)的后缀之一conte的前缀.有了这个前提,采用trie搜索的方法就不难理解了。
2.指定字符串T在字符串S中的重复次数。
方案:用S+’$'构造后缀树,搜索T节点下的叶节点数目即为重复次数
原理:如果T在S中重复了两次,则S应有两个后缀以T为前缀,重复次数就自然统计出来了。
3.字符串S中的最长重复子串
方案:原理同2,具体做法就是找到最深的非叶节点。
这个深是指从root所经历过的字符个数,最深非叶节点所经历的字符串起来就是最长重复子串。
为什么要非叶节点呢?因为既然是要重复,当然叶节点个数要>=2。
4.两个字符串S1,S2的最长公共部分
方案:将S1#S2$作为字符串压入后缀树,找到最深的非叶节点,且该节点的叶节点既有#也有$(无#)。
后缀树的代码实现,下期再续。第二部分、后缀树完。
后缀树的构造
借助后缀树的特性, 我们可以做出一个相当有效的算法. 首先一个重要的特性是: 一朝为叶, 终生为叶. 一个叶节点自诞生以后绝不会有子孙. 更重要的是, 每当我们往树上加入一个新的前缀, 每一条通往叶节点的边都会延长一个字符(新前缀的最后一个字符). 这使得处理通往叶节点的边变得异常简单, 我们完全可以在创建叶节点的时候就把当前字符到文本末的所有字符一股脑塞进去. 是的, 我们不需要知道后面的字符是啥, 但我们知道它们最终都要被加进去. 因此, 一个叶节点诞生的时候, 也正是它可以被我们遗忘的时候. 你可能会担心通往叶节点的边被分割了怎么办, 那也不要紧, 分割之后只是起点变了, 尾部该怎么着还是怎么着.
如此一来, 我们只需要关心显式节点和隐式节点上的更新.
还要提到一个节约时间的方法. 当我们遍历所有后缀时, 如果某个后缀的某个儿子跟待加字符(新前缀最后一个字符)相同, 那么我们当前前缀的所有更新就可以停止了. 如果你理解了后缀树的本质, 你会知道一旦待加字符跟某个后缀的某个儿子相同, 那么更短的后缀必然也有这个儿子. 我们不妨把首个这样的节点定义为结束节点. 比结束节点长的后缀必然是叶节点, 这一点很好解释, 要么本来就是叶节点, 要么就是新创建的节点(新创建的必然是叶节点). 这意味着, 每一个前缀更新完之后, 当前的结束节点将成为下一轮更新的激活节点.
伪代码:
Update( 新前缀 )
{
当前后缀 = 激活节点
待加字符 = 新前缀最后一个字符done = false;
while ( !done ) {
if ( 当前后缀在显式节点结束 )
{if ( 当前节点后没有以待加字符开始的边 ) 在当前节点后创建一个新的叶节点 else done = true;
} else {
if ( 当前隐式节点的下一个字符不是待加字符 ) { 从隐式节点后分割此边 在分割处创建一个新的叶节点 } else done = true; if ( 当前后缀是空后缀 ) done = true; else 当前后缀 = 下一个更短的后缀 }
激活节点 = 当前后缀
}
后缀指针
上面的伪代码看上去很完美, 但它掩盖了一个问题. 注意到第21行, “下一个更短的后缀”, 如果呆板地沿着树枝去搜索我们想要的后缀, 那这种算法就不是线性的了. 要解决此问题, 我们得附加一种指针: 后缀指针. 后缀指针存在于每个结束在非叶节点的后缀上, 它指向“下一个更短的后缀”. 即, 如果一个后缀表示文本的第0到第N个字符, 那么它的后缀指针指向的节点表示文本的第1到第N个字符.
图8是文本ABABABC的后缀树. 第一个后缀指针在表示ABAB的节点上. ABAB的后缀指针指向表示BAB的节点. 同样地, BAB也有它的后缀指针, 指向AB. 如此这般.
加上后缀指针(虚线)的ABABABC的后缀树
介绍一下如何创建后缀指针. 后缀指针的创建是跟后缀树的更新同步的. 随着我们从激活节点移动到结束节点, 我把每个新的叶节点的父亲的路径保存下来. 每当创建一条新边, 我同时也在上一个叶节点的父亲那儿创建一个后缀指针来指向当前新边开始的节点. (显然, 我们不能在第一条新边上做这样的操作, 但除此之外都可以这么做.)
有了后缀指针, 就可以方便地一个后缀跳到另一个后缀. 这个关键性的附加品使得算法的时间上限成功降为O(N)。
有关字符串的一个总结
AC自动机
https://blog.csdn.net/hgqqtql...
https://blog.csdn.net/liu9402...
简介
AC自动机:Aho-Corasickautomation,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识。AC自动机算法分为3步:构造一棵Trie树,构造失败指针和模式匹配过程。
AC自动机(Aho-Corasick automaton)是用来处理多模式匹配问题的。
基本可认为是TrieTree+KMP。其中KMP是一种单模式匹配算法。
AC自动机的构造要点是失败指针的设置,用于匹配失败时跳转到另一节点继续匹配。同时在匹配的过程中也用来检索其他“同尾”的模式。
构造过程
使用Aho-Corasick算法需要三步:
建立模式串的Trie
给Trie添加失败路径
根据AC自动机,搜索待处理的文本
我们以下面这个例子来介绍AC自动机的运作过程
这里以 hdu 2222 KeywordsSearch 这一道题最为例子进行讲解,其中测试数据如下:
给定5个单词:say she shr he her,然后给定一个字符串 yasherhs。问一共有多少单词在这个字符串中出现过。
确定数据结构
struct Node
{
int cnt;//是否为该单词的最后一个结点
Node *fail;//失败指针
Node *next[26];//Trie中每个结点的各个节点
}*queue[500005];//队列,方便用BFS构造失败指针
char s[1000005];//主字符串
char keyword[55];//需要查找的单词
int head,tail;
Node *root;//头结点
第一步构建Trie树
根据输入的 keyword 一 一 构建在Trie树中
void Build_trie(char *keyword)//构建Trie树
{
Node *p,*q;
int i,v;
int len=strlen(keyword);
for(i=0,p=root;inext[v]==NULL)
{
q=(struct Node *)malloc(sizeof(Node));
Init(q);
p->next[v]=q;//结点链接
}
p=p->next[v];//指针移动到下一个结点
}
p->cnt++;//单词最后一个结点cnt++,代表一个单词
}
失败指针的设置
用BFS。对于每个节点,我们可以这样处理:设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个节点,他的儿子中也有字母为C的节点。然后把当前节点的失败指针指向那个字目也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root。
最开始,我们把root加入队列(root的失败指针显然指向自己),这以后我们每处理一个点,就把它的所有儿子加入队列,直到全部设置完毕。
要点1:root的孩子的那一层比较特殊,若按照上述算法,它们的失败指针会指向自己,这会在匹配的过程中导致死循环。显然root的子节点的失败指针应指向root,我们应对这一层单独处理。
要点2:沿着父节点的失败指针走到root之后并不是立即将子节点的失败指针设置为root,而是在root的子节点中找寻字母为C的节点,将它设置为失败指针。若没有才设置为root。这样不会丢失模式只有一个字母的情况。
构建失败指针是AC自动机的关键所在,可以说,若没有失败指针,所谓的AC自动机只不过是Trie树而已。
失败指针原理:
构建失败指针,使当前字符失配时跳转到另一段从root开始每一个字符都与当前已匹配字符段某一个后缀完全相同且长度最大的位置继续匹配,如同KMP算法一样,AC自动机在匹配时如果当前字符串匹配失败,那么利用失配指针进行跳转。由此可知如果跳转,跳转后的串的前缀必为跳转前的模式串的后缀,并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点(跳转后匹配字符数不可能大于跳转前,否则无法保证跳转后的序列的前缀与跳转前的序列的后缀匹配)。所以可以利用BFS在Trie上进行失败指针求解。
失败指针利用:
如果当前指针在某一字符s[m+1]处失配,即(p->next[s[m+1]]==NULL),则说明没有单词s[1...m+1]存在,此时,如果当前指针的失配指针指向root,则说明当前序列的任何后缀不是是某个单词的前缀,如果指针的失配指针不指向root,则说明当前序列s[i...m]是某一单词的前缀,于是跳转到当前指针的失配指针,以s[i...m]为前缀继续匹配s[m+1]。
对于已经得到的序列s[1...m],由于s[i...m]可能是某单词的后缀,s[1...j]可能是某单词的前缀,所以s[1...m]中可能会出现单词,但是当前指针的位置是确定的,不能移动,我们就需要temp临时指针,令temp=当前指针,然后依次测试s[1...m],s[i...m]是否是单词。
简单来说,失败指针的作用就是将主串某一位之前的所有可以与模式串匹配的单词快速在Trie树中找出。
第二步 构建失败指针
在构造完Tire树之后,接下去的工作就是构造失败指针。构造失败指针的过程概括起来就一句话:设这个节点上的字母为C,沿着它父亲节点的失败指针走,直到走到一个节点,它的子结点中也有字母为C的节点。然后把当前节点的失败指针指向那个字母也为C的儿子。如果一直走到了root都没找到,那就把失败指针指向root。具体操作起来只需要:先把root加入队列(root的失败指针指向自己或者NULL),这以后我们每处理一个点,就把它的所有儿子加入队列。
观察构造失败指针的流程:对照图来看,首先root的fail指针指向NULL,然后root入队,进入循环。从队列中弹出root,root节点与s,h节点相连,因为它们是第一层的字符,肯定没有比它层数更小的共同前后缀,所以把这2个节点的失败指针指向root,并且先后进入队列,失败指针的指向对应图中的(1),(2)两条虚线;从队列中先弹出h(右边那个),h所连的只有e结点,所以接下来扫描指针指向e节点的父节点h节点的fail指针指向的节点,也就是root,root->next['e']==NULL,并且root->fail==NULL,说明匹配序列为空,则把节点e的fail指针指向root,对应图中的(3),然后节点e进入队列;从队列中弹出s,s节点与a,h(左边那个)相连,先遍历到a节点,扫描指针指向a节点的父节点s节点的fail指针指向的节点,也就是root,root->next['a']==NULL,并且root->fail==NULL,说明匹配序列为空,则把节点a的fail指针指向root,对应图中的(4),然后节点a进入队列。接着遍历到h节点,扫描指针指向h节点的父节点s节点的fail指针指向的节点,也就是root,root->next['h']!=NULL,所以把节点h的fail指针指向右边那个h,对应图中的(5),然后节点h进入队列...由此类推,最终失配指针如图所示
void Build_AC_automation(Node *root)
{
head=0,tail=0;//队列头、尾指针
queue[head++]=root;//先将root入队
while(head!=tail)
{
Node *p=NULL;
Node *temp=queue[tail++];//弹出队头结点
for(int i=0;i<26;i++)
{
if(temp->next[i]!=NULL)//找到实际存在的字符结点
{ //temp->next[i] 为该结点,temp为其父结点
if(temp==root)//若是第一层中的字符结点,则把该结点的失败指针指向root
temp->next[i]->fail=root;
else
{
//依次回溯该节点的父节点的失败指针直到某节点的next[i]与该节点相同,
//则把该节点的失败指针指向该next[i]节点;
//若回溯到 root 都没有找到,则该节点的失败指针指向 root
p=temp->fail;//将该结点的父结点的失败指针给p
while(p!=NULL)
{
if(p->next[i]!=NULL)
{
temp->next[i]->fail=p->next[i];
break;
}
p=p->fail;
}
//让该结点的失败指针也指向root
if(p==NULL)
temp->next[i]->fail=root;
}
queue[head++]=temp->next[i];//每处理一个结点,都让该结点的所有孩子依次入队
}
}
}
}
显然我们在构建失败指针的时候都是从当前节点的父节点的失败指针出发,由于Trie树将所有单词中相同前缀压缩在了一起,所以所有失败指针都不可能平级跳转(到达另一个与自己深度相同的节点),因为如果平级跳转,很显然跳转所到达的那个节点肯定不是当前匹配到的字符串的后缀的一部分,否则那两个节点会合为一个,所以跳转只能到达比当前深度小的节点,又因为是由当前节点父节点开始的跳转,所以这样就可以保证从root到所跳转到位置的那一段字符串长度小于当前匹配到的字符串长度。另一方面,我们可以类比KMP求NEXT数组时求最大匹配数量的思想,那种思想在AC自动机中的体现就是当构建失败指针时不断地回到之前的跳转位置,然后判断跳转位置的下一个字符是否包含当前字符,如果是就将失败指针与那个跳转位置连接,如果跳转位置指向NULL就说明当前匹配的字符在当前深度之前没有出现过,无法与任何跳转位置匹配,而若是找到了第一个跳转位置的下一个字符包含当前字符的的跳转位置,则必然取到了最大的长度,这是因为其余的当前正在匹配的字符必然在第一个跳转位置的下一个字符包含当前字符的的跳转位置深度之上,而那样的跳转位置就算可以,也不会是最大的(最后一个字符的深度比当前找到的第一个可行的跳转位置的最后一个字符的深度小,串必然更短一些)。
匹配过程
最后,我们便可以在AC自动机上查找模式串中出现过哪些单词了。匹配过程分两种情况:(1)当前字符匹配,表示从当前节点沿着树边有一条路径可以到达目标字符,此时只需沿该路径走向下一个节点继续匹配即可,目标字符串指针移向下个字符继续匹配;(2)当前字符不匹配,则去当前节点失败指针所指向的字符继续匹配,匹配过程随着指针指向root结束。重复这2个过程中的任意一个,直到模式串走到结尾为止。
对例子来说:其中模式串为yasherhs。对于i=0,1。Trie中没有对应的路径,故不做任何操作;i=2,3,4时,指针p走到左下节点e。因为节点e的count信息为1,所以cnt+1,并且讲节点e的count值设置为-1,表示改单词已经出现过了,防止重复计数,最后temp指向e节点的失败指针所指向的节点继续查找,以此类推,最后temp指向root,退出while循环,这个过程中count增加了2。表示找到了2个单词she和he。当i=5时,程序进入第5行,p指向其失败指针的节点,也就是右边那个e节点,随后在第6行指向r节点,r节点的count值为1,从而count+1,循环直到temp指向root为止。最后i=6,7时,找不到任何匹配,匹配过程结束。
一开始,Trie中有一个指针t1指向root,待匹配串中有一个指针t2指向串头。接下来的操作和KMP很相似:
若:t2指向的字母,是Trie树中,t1指向的节点的儿子,那么
①t2+1,t1改为那个儿子的编号
②如果t1所在的点可以顺着失败指针走到一个绿色点(指TrieTree中单词结尾字母对应的节点),那么以那个绿点结尾的单词就算出现过了。
否则:t1顺这当前节点的失败指针向上找,直到t2是t1的一个儿子,或者t1指向根。如果t1路过了一个绿色的点,那么以这个点结尾的单词就算出现过了。
int query(Node *root)
{ //i为主串指针,p为模式串指针
int i,v,count=0;
Node *p=root;
int len=strlen(s);
for(i=0;inext[v]==NULL && p!=root)
p=p->fail;
p=p->next[v];//找到后p指针指向该结点
if(p==NULL)//若指针返回为空,则没有找到与之匹配的字符
p=root;
Node *temp=p;//匹配该结点后,沿其失败指针回溯,判断其它结点是否匹配
while(temp!=root)//匹配结束控制
{
if(temp->cnt>=0)//判断该结点是否被访问
{
count+=temp->cnt;//由于cnt初始化为 0,所以只有cnt>0时才统计了单词的个数
temp->cnt=-1;//标记已访问过
}
else//结点已访问,退出循环
break;
temp=temp->fail;//回溯 失败指针 继续寻找下一个满足条件的结点
}
}
return count;
}
C++实现
//TrieTreeNode.h
#pragma once
#include
using namespace std;
template
class TrieTreeNode
{
public:
TrieTreeNode(int MaxBranch)//用于构造根节点
{
MaxBranchNum = MaxBranch;
ChildNodes = new TrieTreeNode*[MaxBranchNum];
for (int i = 0; i < MaxBranchNum; i++)
ChildNodes[i] = NULL;
word = NULL;
wordlen = 0;
FailedPointer = NULL;
Freq = 0;
ID = -1;
}
public:
int MaxBranchNum;//最大分支数;
char* word;//单词字符串的指针
int wordlen;
TrieTreeNode **ChildNodes;
int Freq;//词频统计
int ID;//构建TrieTree树时的插入顺序,可用来记录字符串第一次出现的位置
TrieTreeNode *FailedPointer;
};
//TrieTree.h
#pragma once
#include
#include"TrieTreeNode.h"
#include
using namespace std;
template
class TrieTree
{
//Insert时为节点代表的单词word分配内存,Delete时只修改Freq而不删除word,Search时以Freq的数值作为判断依据,而不是根据word是否为NULL
public:
TrieTree(const int size);
~TrieTree(){ Destroy(root); };
void Insert(const T* str);//插入单词str
void Insert(const T* str, const int num);//插入单词str,带有编号信息
int Search(const T* str);//查找单词str,返回出现次数
bool Delete(const T* str);//删除单词str
void PrintALL();//打印trie树中所有节点对应的单词
void PrintPre(const T* str);//打印以str为前缀的单词
void SetFailedPointer();//设置匹配失效时的跳转指针
int MatchKMP(char* str);//返回str中出现在该TrieTree中的单词个数
private:
void Print(const TrieTreeNode* p);
void Destroy(TrieTreeNode* p);//由析构函数调用,释放以p为根节点的树的空间
private:
TrieTreeNode* root;
int MaxBranchNum;//最大分支数
};
template
void TrieTree::Destroy(TrieTreeNode* p)
{
if (!p)
return;
for (int i = 0; i < MaxBranchNum; i++)
Destroy(p->ChildNodes[i]);
if (!p->word)
{
delete[] p->word;//只是释放了char数组word的空间,指针word本身的空间未释放,由后续的delete p释放
p->word = NULL;
}
delete p;//释放节点空间
p = NULL;//节点指针置为空
//以上的置NULL的两句无太大意义,但是:编程习惯
}
template
bool TrieTree::Delete(const T* str)
{
TrieTreeNode* p = root;
if (!str)
return false;
for (int i = 0; str[i]; i++)
{
int index = str[i] - 'a';
if (p->ChildNodes[index])
p = p->ChildNodes[index];
else return false;
}
p->Freq = 0;
p->ID = -1;
return true;
}
template
void TrieTree::PrintPre(const T* str)
{
TrieTreeNode* p = root;
if (!str)
return;
for (int i = 0; str[i]; i++)
{
int index = str[i] - 'a';
if (p->ChildNodes[index])
p = p->ChildNodes[index];
else return;
}
cout << "以" << str << "为前缀的单词有:" << endl;
Print(p);
}
template
int TrieTree::Search(const T* str)
{
TrieTreeNode* p = root;
if (!str)
return -1;
for (int i = 0; str[i]; i++)
{
int index = str[i] - 'a';
if (p->ChildNodes[index])
p = p->ChildNodes[index];
else return 0;
}
return p->Freq;
}
template
TrieTree::TrieTree(const int size)
{
MaxBranchNum = size;
root = new TrieTreeNode(MaxBranchNum);//根节点不储存字符
root->FailedPointer = root;//设置失配指针
}
template
void TrieTree::Insert(const T* str)
{
TrieTreeNode* p = root;
int i;
for (i = 0; str[i]; i++)
{
if (str[i]<'a' || str[i]>'z')
{
cout << "格式错误!" << endl;
return;
}
int index = str[i] - 'a';//下溯的分支编号
if (!p->ChildNodes[index])
p->ChildNodes[index] = new TrieTreeNode(MaxBranchNum);
p = p->ChildNodes[index];
}
if (!p->word)//该词以前没有出现过
{
p->word = new char[strlen(str) + 1];
strcpy_s(p->word, strlen(str) + 1, str);
p->wordlen = i;//设置单词长度
}
p->Freq++;
}
template
void TrieTree::Insert(const T* str, const int num)
{
TrieTreeNode* p = root;
int i;
for (i = 0; str[i]; i++)
{
if (str[i]<'a' || str[i]>'z')
{
cout << "格式错误!" << endl;
return;
}
int index = str[i] - 'a';//下溯的分支编号
if (!p->ChildNodes[index])
p->ChildNodes[index] = new TrieTreeNode(MaxBranchNum);
p = p->ChildNodes[index];
}
if (!p->word)//该词以前没有出现过
{
p->word = new char[strlen(str) + 1];
strcpy_s(p->word, strlen(str) + 1, str);
p->wordlen = i;
}
p->Freq++;
if (num < p->ID || p->ID == -1)//取最小的num作为当前节点代表的单词的ID
p->ID = num;
}
template
void TrieTree::PrintALL()
{
Print(root);
}
template
void TrieTree::Print(const TrieTreeNode* p)
{
if (p == NULL)
return;
if (p->Freq > 0)
{
cout << "单词:" << p->word << " 频数:" << p->Freq;
if (p->ID >= 0)
cout << " ID:" << p->ID;
cout << endl;
}
for (int i = 0; i < MaxBranchNum; i++)
{
if (p->ChildNodes[i])
{
Print(p->ChildNodes[i]);
}
}
}
template
int TrieTree::MatchKMP(char* str)
{
int count = 0;//str中出现的TrieTree中的单词个数
char* p = str;//str中指针
TrieTreeNode* node = root;//TrieTree的节点指针
while (*p)
{
if (node->ChildNodes[*p - 'a'])//当前字符匹配成功
{
TrieTreeNode* temp = node->ChildNodes[*p - 'a']->FailedPointer;
while (temp != root)//在匹配的情况下,仍然沿FailedPointer搜索,可检索出所有模式。
{
if (temp->Freq > 0)
{
count++;
//cout << "temp->wordlen:" << temp->wordlen << endl;
cout << (int)(p - str) - temp->wordlen + 1 << " " << temp->word << endl;//打印已匹配的模式的信息
}
temp = temp->FailedPointer;
}
node = node->ChildNodes[*p - 'a'];
p++;
if (node->Freq > 0)
{
count++;
//cout << "node->wordlen:" << node->wordlen << endl;
cout << (int)(p - str) - node->wordlen << " " << node->word << endl;//打印已匹配的模式的信息
}
}
else//失配,跳转
{
if (node == root)
p++;
else
node = node->FailedPointer;
}
}
return count;
}
template
void TrieTree::SetFailedPointer()
{
queue*> q;
q.push(root);
while (!q.empty())
{
TrieTreeNode* father = q.front();//父节点
q.pop();
for (int i = 0; i < MaxBranchNum; i++)//对每一个子节点设置FailedPointer
{
if (father->ChildNodes[i])
{
TrieTreeNode* child = father->ChildNodes[i];
q.push(child);
TrieTreeNode* candidate = father->FailedPointer;//从father->FailedPointer开始游走的指针
while (true)
{
if (father == root)
{
candidate = root;
break;
}
if (candidate->ChildNodes[i])//有与child代表的字母相同的子节点
{
candidate = candidate->ChildNodes[i];
break;
}
else
{
if (candidate == root)
break;
candidate = candidate->FailedPointer;//以上两句顺序不能交换,因为在root仍可以做一次匹配
}
}
child->FailedPointer = candidate;
}
}
}
}
//main.cpp
#pragma once
#include
#include
#include"TrieTree.h"
using namespace std;
void test(TrieTree* t)
{
char* charbuffer = new char[50];
char* cb = charbuffer;
fstream fin("d:\\words.txt");
if (!fin){
cout << "File open error!\n";
return;
}
char c;
int num = 0;
while ((c = fin.get()) != EOF)
{
if (c >= '0'&&c <= '9')
num = num * 10 + c - '0';
if (c >= 'a'&&c <= 'z')
*cb++ = c;
if (c == '\n')
{
*cb = NULL;
t->Insert(charbuffer, num);
cb = charbuffer;
num = 0;
}
}
fin.close();
}
void main()
{
TrieTree* t = new TrieTree(26);
char* c1 = "she";
char* c2 = "shee";
char* c3 = "he";
char* c4 = "e";
char* s = "shee";//要匹配的串
t->Insert(c1);
t->Insert(c2);
t->Insert(c3);
t->Insert(c4);
//test(t);
t->SetFailedPointer();
t->PrintALL();
cout << endl << "匹配结果为:" << endl;
int result = t->MatchKMP(s);
cout << "共匹配" << result << "处模式串" << endl;
system("pause");
}
堆
简介
堆实际上是一个完全二叉树,在实现的时候采用数组来实现。
根节点的值比所有节点的值都大, 称为最大堆;
根节点的值比所有节点的值都小, 称为最小堆;
堆的插入和删除
/************************************************************************/
/* 堆的实现 */
/************************************************************************/
#include
namespace stl
{
template
class Heap
{
public:
/************************************************************************/
/*构造函数*/
Heap(int capacity = 100)
:size(0) //堆中包含数据个数
{
H.resize(capacity);
}
~Heap()
{
}
bool isEmpty()
{
return size == 0;
}
void makeEmpty()
{
size = 0;
for (auto it = H.begin(); it != H.end(); it++)
{
H.erase(it);
}
}
/************************************************************************/
/*插入函数 */
void insert(const T & x)
{
//如果vector中已经存满了,重新分为大小,数组首地址不存数据
if (size == H.size() -1)
{
H.resize(2*size);
}
//size大小加一
for (int current = ++size; current > 1 && H[current/2] > x; current /= 2)
{
H[current] = H[current/2];
}
//找到空位将x插入
H[current] = x;
}
/*删除函数*/
T deleteMin()
{
if (isEmpty())
{
throw();
}
int current, child;
T returnVal = H[1];
T lastElement = H[size--]; //将最后一个值保存下来,删除一个元素所以自减运算
for (current = 1; 2 * current > size; current = child)
{
child = 2 * current;
//防止访问越界
if (child != size && H[2 * current] > H[2 * current + 1])
{
++child;
}
//比较子较小的儿子与最后一个值的大小,如果儿子较小用儿子上滤,否则跳出循环
if (H[child] < lastElement)
{
H[current] = H[child];
}
else
{
break;
}
}
H[current] = lastElement;
return returnVal;
}
private:
std::vector H;
int size;
};
}
堆排序
https://blog.csdn.net/pursue_...
将堆的顶部,与最后一个元素交换。 此时除了最后一个元素外, 剩下元素所组成的树已经不是 堆了。(因为此时顶部的元素可能比较小)。 所以, 要将剩下的元素通过 adjust函数调整成 堆。
然后继续将剩余元素中的最后一个元素 与 新堆的顶部交换
#include
using namespace std;
void adjust_heap(int* a, int node, int size)
{
int left = 2*node + 1;
int right = 2*node + 2;
int max = node;
if( left < size && a[left] > a[max])
max = left;
if( right < size && a[right] > a[max])
max = right;
if(max != node)
{
swap( a[max], a[node]);
adjust_heap(a, max, size);
}
}
void heap_sort(int* a, int len)
{
for(int i = len/2 -1; i >= 0; --i)
adjust_heap(a, i, len);
for(int i = len - 1; i >= 0; i--)
{
swap(a[0], a[i]); // 将当前最大的放置到数组末尾
adjust_heap(a, 0 , i); // 将未完成排序的部分继续进行堆排序
}
}
int main()
{
int a[10] = {3, 2, 7, 4, 2, -999, -21, 99, 0, 9 };
int len= sizeof(a) / sizeof(int);
for(int i = 0; i < len; ++i)
cout << a[i] << ' ';
cout << endl;
heap_sort(a, len);
for(int i = 0; i < len; ++i)
cout << a[i] << ' ';
cout << endl;
return 0;
}
然而堆排序中的几个函数,还是可以通过优化来提升 时间复杂度的。比如 adjust()函数的优化
如果在用 adjust在调整最大堆时, 交换需要以下操作:
temp = a[node];
a[node] = a[max];
a[max] = temp;
需要操作三次,100000次 的交换需要操作 300000次。太过于耗时。所以我们可以做类似于插入排序的优化。
if( 节点的某一个儿子 > 节点本身 )
用 temp 把儿子存起来, 并把节点赋值给儿子;
用temp上浮一层到节点的位置, 继续执行判断, 这样一层层不停的上浮。直到不符合条件
if( temp < 同一层的兄弟节点 || temp < 父亲节点)
把 temp 赋值给当前层的元素;
这样省去了每次的交换, 100000的操作只需要操作100000次。大大提高了效率
优先队列
简介
优先队列是计算机科学中的一类抽象数据类型。优先队列中的每个元素都有各自的优先级,优先级最高的元素最先得到服务;优先级相同的元素按照其在优先队列中的顺序得到服务。优先队列往往用堆来实现。
出于性能考虑,优先队列用堆来实现,具有O(log n)时间复杂度的插入元素性能,O(n)的初始化构造的时间复杂度。如果使用自平衡二叉查找树,插入与删除的时间复杂度为O(log n),构造二叉树的时间复杂度为O(n log n)。从计算复杂度的角度,优先级队列等价于排序算法。
用堆来满足先进先出,按照优先级排序,然后输出
分析:
1.一棵完全二叉树,所以可以用一个数组表示而不需要用指针。但是用数组就要事先估计堆的大小,所以用一个Capacity表示最大值。
2.因为保持堆序性质,最小元就是在根上,删除后还得做一些调整来保持完全二叉树的结构(实际就是删除最后一个元素,然后把它插入到队列中)。
3.若当前节点下标为i,则i2和i2+1就是它的左右孩子,我们在data[0]处放了一个MIN_INT。
4.入队:首先size++,如果data[size]插入不破坏结构,结束。否则,不断用父亲的值赋给当前节点,直到合适的位置。时间复杂度O(logN)。
5.出队:首先根节点置空,记录size点(last)的值然后size–,last能放入当前空穴就结束,否则,不断用儿子中最小的值赋给当前节点。时间复杂度O(logN)。(注释: 入队和出队的调整是一个逆向对称的过程)
6.队首:直接返回根节点的值,为最小值,O(1)。
Code1
#include
using namespace std;
class PriorityQueue
{
private:
int* pArray;
int m_length;
public:
PriorityQueue(int N) {
// 为后续根节点直接从1开始作准备
pArray = new int[N + 1];
m_length = 0;
}
int delMax() {
// 大根堆第一个元素为最大
int max = pArray[1];
// 将第一个元素和最后一个元素交换,并使长度减一,即删除最大的元素
swap(pArray[1], pArray[m_length--]);
// 防止对象游离
pArray[m_length + 1] = NULL;
// 下沉恢复堆的有序性
sink(1);
// 返回最大的节点值
return max;
}
void insert(int v) {
// 将值v插入到pArray[1]位置处,所以这里用的前置++
pArray[++m_length] = v;
// 新加入的元素上浮
swim(m_length);
}
// 判断是否为空
bool isEmpty() {
return m_length == 0;
}
int size() {
return m_length;
}
// 向上浮
void swim(int k) {
// 判断最下层的叶子节点值如果大于其父节点则进入循环上浮
while (k > 1 && pArray[k] > pArray[k / 2]) {
// 交换父节点和子节点
swap(pArray[k / 2], pArray[k]);
// k数值减小继续向上浮
k /= 2;
}
}
// 向下沉
void sink(int k) {
while (2 * k <= m_length)
{
// 由于堆的性质父节点为k则其左子树为2*k即j
int j = 2 * k;
// 这里先比较左子树和右子树的大小,将最大的那个键记录下来再和父节点比较
if (j < m_length && (pArray[j] < pArray[j + 1])) j++;
// 和父节点比较如果父节点比最大的子节点还要大,则直接退出循环
if (pArray[k] > pArray[j]) break;
// 如果父节点比子节点小则交换
swap(pArray[k], pArray[j]);
// k值变大继续下沉
k = j;
}
}
};
int main() {
PriorityQueue pq(5);
pq.insert(2);
pq.insert(11);
pq.insert(6);
pq.insert(1);
pq.insert(15);
cout << pq.delMax() << endl;
cout << pq.delMax() << endl;
cout << pq.delMax() << endl;
cout << pq.delMax() << endl;
cout << pq.delMax() << endl;
return 0;
}
Code2
#ifndef PRIORITYQUEUE_H
#define PRIORITYQUEUE_H
template
class PriorityQueue
{
private:
int Capacity = 100; //队列容量
int size; //队列大小
T* data; //队列变量
public:
PriorityQueue();
~PriorityQueue();
int Size();
bool Full(); //判满
bool Empty(); //判空
void Push(T key); //入队
void Pop(); //出队
void Clear(); //清空
T Top(); //队首
};
template
PriorityQueue::PriorityQueue()
{
data = (T*) malloc((Capacity + 1)*sizeof(T));
if(!data)
{
perror("Allocate dynamic memory");
return;
}
size = 0;
}
template
PriorityQueue::~PriorityQueue()
{
while(!Empty())
{
Pop();
}
}
template
//判空
bool PriorityQueue::Empty()
{
if(size > 0)
{
return false;
}
return true;
}
template
//清空
void PriorityQueue::Clear()
{
while(!Empty())
{
Pop();
}
}
template
//判满
bool PriorityQueue::Full()
{
if(size == Capacity)
{
return true;
}
return false;
}
template
//大小
int PriorityQueue::Size()
{
return size;
}
template
//入队
void PriorityQueue::Push(T key)
{
//空队则直接入队
if(Empty())
{
data[++size] = key;
return;
}
int i;
if(Full())
{
perror("Priority queue is full\n");
return;
}
for(i = ++size; data[i/2] > key; i /= 2)
{
data[i] = data[i/2];
}
data[i] = key;
}
template
//队首
T PriorityQueue::Top()
{
if(Empty())
{
perror("Priority queue is full\n");
return data[0];
}
return data[1];
}
template
//出队, 取完队首元素后,才执行出队操作,即去除堆顶元素,将末尾元素防止堆顶,并做sink调整
void PriorityQueue::Pop()
{
int i, child;
T min, last;
if(Empty())
{
perror("Empty queue\n");
return;
}
// min = data[1];
last = data[size--];
for(i = 1; i * 2 <= size; i = child)
{
child = i * 2;
if(child != size && data[child + 1] < data[child])
{
child++;
}
if(last > data[child])
{
data[i] = data[child];
} else
{
break;
}
}
data[i] = last;
}
#endif // PRIORITYQUEUE_H
//.cpp
#include
#include
#include "priorityqueue.h"
using namespace std;
const int MAXN = 1000;
//事件
struct Event
{
int arrive; //到达时间
int service; //服务时间
Event(){};
Event(int a, int s)
{
arrive = a;
service = s;
}
} cus[MAXN];
int operator > (const Event& a, const Event& b)
{
return a.arrive > b.arrive;
}
int operator < (const Event& a, const Event& b)
{
return a.arrive < b.arrive;
}
int main()
{
Event minCustomer(INT_MIN, 0);
PriorityQueue request; //顾客
int n, time;
while (cin >> n)
{
for (int i = 0; i < n; ++i)
{
cin >> cus[i].arrive >> cus[i].service;
request.Push(cus[i]); //顾客入队
}
time = 0; //服务时间
while (!request.Empty())
{
Event current = request.Top();
request.Pop();
//计算最后的时间节点
time = max(current.arrive + current.service, time + current.service);
}
cout << "time costs = " << time << endl;
}
return 0;
}