1、二叉搜索树(排序树、查找树)的搜索、插入、删除,时间复杂度?
目的:提高查找和插入删除关键字的速度,不是为了排序。
(1)性质:
若它的左子树不为空,则左子树上所有节点的值均小于它的根结构的值。
若它的右子树不为空,则右子树上所有节点的值均大于它的根节点的值。
它的左右子树也分别为二叉排序树。
相关代码如下:
#include <iostream>
using namespace std;
struct Node
{
int data;
Node* lchild;
Node* rchild;
Node():lchild(NULL),rchild(NULL){}
};
void CreateTree(Node *&T)
{
int data;
cin>>data;
if(data==0)
return;
T=new Node();
T->data=data;
CreateTree(T->lchild);
CreateTree(T->rchild);
}
void PrintTree(Node *T)
{
if(T==NULL)
return;
cout<<T->data<<" ";
PrintTree(T->lchild);
PrintTree(T->rchild);
}
bool Search(Node *T,int key,Node *&Pos,Node *Parent)
{
if(T==NULL)
{
Pos=Parent;
return false;
}
if(T->data==key)
{
return true;
}
else if(T->data>key)
{
Search(T->lchild,key,Pos,T);
}
else
Search(T->rchild,key,Pos,T);
}
void Insert(Node *&T,int key)
{
if(T==NULL)
{
T->data=key;
return;
}
Node *p;
if(Search(T,key,p,T))
return;
if(p->data>key)
{
Node *s=new Node();
s->data=key;
p->lchild=s;
}
else
{
Node *s=new Node();
s->data=key;
p->rchild=s;
}
}
void DeleteNode(Node *&T,int key)
{
if(T->lchild==NULL)
{
Node *p=T;
T=T->rchild;
delete p;
}
else if(T->rchild==NULL)
{
Node *p=T;
T=T->lchild;
delete p;
}
else
{
Node *p=T;
Node *s=T->lchild;
while(s->rchild)
{
p=s;//p指向s的前一个节点,用来接s的左子树
s=s->rchild;
}
T->data=s->data;
if(p==T)//说明没有进入while循环,p位置没变,s是p的左子树
p->lchild=s->lchild;
else//说明p的位置改变,s是p的右子树
p->rchild=s->lchild;
delete s;
}
}
void Delete(Node *&T,int key)
{
if(T==NULL)
return;
if(T->data==key)
DeleteNode(T,key);
else if(T->data>key)
Delete(T->lchild,key);
else
Delete(T->rchild,key);
}
int main()
{
Node *T;
CreateTree(T);
PrintTree(T);
cout<<endl;
Insert(T,7);
Insert(T,10);
Insert(T,11);
Insert(T,1);
PrintTree(T);
cout<<endl;
Delete(T,3);
PrintTree(T);
system("pause");
return 0;
}
3 2 0 0 8 0 9 0 0
3 2 8 9
3 2 1 8 7 9 10 11
2 1 8 7 9 10 11 请按任意键继续. . .
图:
思想如下:
(1)查找,若找到,则返回,若没有,则返回其所在位置的父节点指针。
(2)插入,根据查找的结果,比较父节点和要插入元素的大小,若比该元素小,则将该元素作为右子树插入,否则作为左子树插入。
(3)删除,有3种情况,左子树为空,则重接右子树,若右子树为空则重接左子树,否则,说明节点T存在左右子树,此时,指向左子树s,s再向右走到尽头,
并保存s父节点q的位置。最后将s的左子树接回去。此时分为两种情况:
a.刚开始s没有右子树(q==T),只要将s的左子树作为q的左孩子。
b.s有右子树(q!=T)讲s的左子树作为q的右孩子。
注意:插入的时候,要记住插入位置的父节点,但是删除的时候,只需要修改该指针的值即可如(修改了指针T->lchlid的值)。然后把原来指针指向的内容释放掉。
2、二叉平衡树插入节点的原理,有哪几种旋转方式?分别适用于哪种情况,分析二叉平衡树的时间复杂度。
平衡二叉树是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1。其中左子树深度减去右子树深度为平衡因子,BF取值为-1,0,1。最小不平衡子树的概念。
原理:插入结点,若破坏了平衡性,则找出最小不平衡子树,进行旋转,成为新的平衡子树。
时间复杂度为logn,相当于是折半查找。见后面的说明。
AVL树的平衡插入操作
#include <iostream>
using namespace std;
struct Node
{
int value;
int bf;
Node *lchild;
Node *rchild;
Node(){lchild=NULL;rchild=NULL;}
};
void R_Rotate(Node *&T)
{
Node *p=T->lchild;
T->lchild=p->rchild;
p->rchild=T;
T=p;
}
void L_Rotate(Node *&T)
{
Node *p=T->rchild;
T->rchild=p->lchild;
p->lchild=T;
T=p;
}
void LeftBalance(Node *&T)
{
Node *p=T->lchild;
switch(p->bf)
{
case 1://说明插在左孩子的左子树下,需要右旋转
T->bf=p->bf=0;
R_Rotate(T);
break;
case -1://需要做双旋转
Node *pr=p->rchild;
switch(pr->bf)
{
case 1:
T->bf=-1;
p->bf=0;
break;
case 0://这种情况是pr是刚插入的,所以没有子树
T->bf=p->bf=0;
break;
case -1:
T->bf=0;
p->bf=1;
break;
}
pr->bf=0;
L_Rotate(T->lchild);
R_Rotate(T);
}
}
void RightBalance(Node *&T)
{
Node *p=T->rchild;
switch(p->bf)
{
case -1:
T->bf=p->bf=0;
L_Rotate(T);
break;
case 1://需要做双旋转
Node *pl=p->lchild;
switch(pl->bf)
{
case -1:
T->bf=1;
p->bf=0;
break;
case 0://这种情况是pl是刚插入的,所以没有子树
T->bf=p->bf=0;
break;
case 1:
T->bf=0;
p->bf=-1;
break;
}
pl->bf=0;
R_Rotate(T->rchild);
L_Rotate(T);
}
}
bool InsertAVL(Node *&T,bool &Taller,int key)
{
if(T==NULL)
{
T=new Node();
T->bf=0;
T->value=key;
Taller=true;
return true;
}
else
{
if(T->value==key)
{
Taller=false;
return false;
}
if(T->value>key)
{
if(!InsertAVL(T->lchild,Taller,key))
return false;
if(Taller)//说明在左子树下插入成功
{
switch(T->bf)
{
case 1://说明原来左边就高
LeftBalance(T);//平衡旋转之后高度不变,且能够修改相关的平衡因子
Taller=false;
break;
case 0://原来相等
T->bf=1;
Taller=true;//只有这个时候是长高了的
break;
case -1:
T->bf=0;
Taller=false;
break;
}
}
}
else if(T->value<key)
{
if(!InsertAVL(T->rchild,Taller,key))
return false;
if(Taller)//说明在右子树下插入成功
{
switch(T->bf)
{
case -1://说明原来右边就高
RightBalance(T);//平衡旋转之后高度不变,且能够修改相关的平衡因子
Taller=false;
break;
case 0://原来相等
T->bf=-1;
Taller=true;//只有这个时候是长高了的
break;
case 1:
T->bf=0;
Taller=false;
break;
}
}
}
return true;
}
}
void PrintTree(Node *T)
{
if(T!=NULL)
{
cout<<T->value<<" ";
PrintTree(T->lchild);
PrintTree(T->rchild);
}
}
int main()
{
int a[10]={3,2,1,4,5,6,7,10,9,8};
Node *T=NULL;
bool Taller;
for(int i=0;i<10;i++)
{
InsertAVL(T,Taller,a[i]);
}
PrintTree(T);
system("pause");
}
代码调试成功
插入思想:对于任意一个节点,都要看在其子树下面插入是否有长高,那么根据在左右子树插入,以及原来平衡因子的情况。可以进行平衡处理或者修改平衡引子的操作。
平衡思想:对于一个需要平衡处理的子树T,比如要左平衡处理,那么根据左子树p原来的情况进行操作,
如果为1,那么修改平衡引子,直接右旋。
如果为-1,那么需要双旋转。根据下面的右子树pr(-1,0,1)来进行判断,来修改平衡因子,注意为0的时候是刚插入的,然后先对子树左旋,再整个右旋。无论pr是哪种情况
,旋转之后其平衡因子都为0.而为pr为0的时候pr/T平衡因子也为0
右旋:旋转为左孩子的右孩子,并做相应的操作。
3、红黑树的定义,红黑树的性能分析和与二叉平衡树的比较。
参考:http://blog.csdn.net/v_JULY_v/article/category/774945
http://blog.csdn.net/silangquan/article/details/18655795
红黑树和AVL类似,都是在进行插入删除的时候通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。其统计性能比AVL树要好。
红黑树与AVL树的区别?
(1)前者利用颜色来识别来标识结点的高度,追求的局部平衡,而不是AVL树种的非常严格平衡(STL中的关联式容器默认右红黑树实现)
(2)红黑树是AVL树的变种,通过着色法确保:没有一条路径会比其他路径长出两倍,故而达到接近平衡的目的。
(3)性质:
1、每个节点不是红色就是黑色。
2、根节点为黑色。
3、若节点为红色,其子节点必为黑色。
4、任意一个节点,到NULL(树尾端)黑色节点数必须相同。
上述规则保证了这个树大致平衡,也决定了红黑树的插入、删//查询快速。
(4)插入:
根据规则4,新增节点必须为红。
:黑父:
若新节点的父节点为黑色,那么插入一个红节点,不会影响红黑树的平衡。故而插入完成。
红父:
若父为红,则祖父必为黑,根据叔来调整。
1、红叔:
无需进行旋转,只需将父与叔变为黑色,祖父变红。但由祖父节点的父节点可能为红,违3.此时必须将祖父节点作为新的判定点,向上(迭代)。
2、黑叔:
分以下4种情况:父亲和孩子都在左边,父亲和孩子都在右边,此时做一次旋转即可。而另外2种情况则是将新节点做为根黑节点,将祖父变红,作为其子树。
可以看出,哪个最为最后的根节点,则变为黑色,而原来的根节点则变为红色
插入操作:解决的是红→红的问题。
(5)删除:
对于平衡状态下的红黑树,要么是单支黑红,要么是有两个节点。以这个为原则。
a、若删除的节点为红色,则删除后红黑性质不会被破坏,操作结束。单支黑红。
b、若为黑色则红黑性质会破坏,需要相应的调整。
以有两个子树的情况为例:和二叉搜索树一样,找到右子树中最小的点,将值做替换,但是颜色不交换,因此把问题转移到删除右子树中最小的节点的问题。记为y
,y没有左子树,其右孩子为x假设有的话。
情况1,当y为红色时,删除无影响。
情况1,当y为黑色,x为红色时,将y删除,x移到y的位置,将x颜色变红。
情况2,当y为黑色,x为黑色时,则要看原来y位置兄弟的情况。
不过总体思路是:
若兄弟为红,改变父兄颜色,左旋转。转到2.
兄弟为黑,且侄子为红,改变兄弟为红,达到平衡,相当于少了一个黑色节点。
兄弟为黑,左侄子为红,右侄子为黑,则兄弟右旋。转到4.
兄弟为黑,左侄子任意,右侄子为红,则交换父兄颜色,父节点左旋,相当于左边加了一个黑节点。
情况1:x的兄弟w为红色,则w的儿子必然全黑,w父亲p也为黑。
改变p与w的颜色,同时对p做一次左旋,这样就将情况1转变为情况2,3,4的一种。
情况2:x的兄弟w为黑色,x与w的父亲颜色可红可黑。
因为x子树相对于其兄弟w子树少一个黑色节点,可以将w置为红色,这样,x子树与w子树黑色节点一致,保持了平衡。
new x为x与w的父亲。new x相对于它的兄弟节点new w少一个黑色节点。如果new x为红色,则将new x置为黑,则整棵树平衡。否则,
情况2转换为情况1,3,4 情况2转变为情况1,2,3,4.
情况3:w为黑色,w左孩子红色,右孩子黑色。
交换w与左孩子的颜色,对w进行右旋。转换为情况4
情况4:w为黑色,右孩子为红色。
交换w与父亲p颜色,同时对p做左旋。这样左边缺失的黑色就补回来了,同时,将w的右儿子置黑,这样左右都达到平衡。
个人认为这四种状况比较难以理解,总结了一下。情况2是最好理解的,减少右子树的一个黑色节点,使x与w平衡,将不平衡点上移至x与w的父亲。
进行下一轮迭代。情况1:如果w为红色,通过旋转,转成成情况1,2,3进行处理。而情况3转换为情况4进行处理。也就是说,情况4是最接近最终解
的情况。情况4:右儿子是红色节点,那么将缺失的黑色交给右儿子,通过旋转,达到平衡。
4、B树、B+树、Trie的概念及用途,添加删除节点的原理。
多路查找树:一个节点的孩子数可以多于两个,且一个节点处可以存储多个元素。
(1)2-3树
其中的每一个节点都具有两个孩子或者三个孩子。
一个2节点包含一个元素和2个孩子(或没有孩子)
一个3节点包含一小一大两个元素和三个孩子(或没有孩子)
图:
插入实现:需要进行一些调整。
(2)2-3-4树
包括了4节点,包含小中大三个元素和4个孩子(或者没有孩子)
(3)B树
B树是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例,节点最大的孩子数目为B树的阶(order)如2-3-4树是4阶B树。
一个m阶的B树具有如下性质:
a、如果根节点不是叶节点,则其至少有两棵子树。
b、每一个非根的分支节点都有k-1个元素和k个孩子。其中[m/2]<=k<=m。 [x] 注意上面闭下面开。为不小于x的最小整数。而每一个叶子节点有k-1个元素
c、所有叶子节点都位于同一层次。
d、所有分支节点包含下列信息数据
如图:
对于n个关键字的m阶B树,最坏情况要查找几次?
第一层至少有一个节点,那么第二层至少有2个节点,根据性质b,那么第三层有2*([m/2])个节点,而第k层有2*([m/2])的k-2次方个节点。而对于第k层有2*([m/2])的
k-1次方个节点,而k+1层就是叶子节点,当找到叶子节点时,实际上有n+1个节点是查找不成功的。故n+1>=2*([m/2])的k-1次方。从而可以推出:
k<=log[m/2]((n+1)/2)+1
也就是说查找次数不超过这么多次。
应用场合:
主要用于大量数据的查找工作。将每个节点看成硬盘不同页面,这样讲根节点放在内存中,这样找其余节点时,只需要去外存找几次就可以了。(页面换入换出)
(4)B+树
和B树的差异在于:
a、有n棵子树的节点中包含有n个关键字。
b、所有的叶子节点包含全部关键字的信息,及指向含这些关键字记录的指针,叶子节点本身依关键字的大小自小而大的顺序链接。
c、所有分支节点可以看成是索引,节点中仅含有其子树中的最大(或最小)关键字。
如果是随机查找,则从根节点出发,与B树查找方式相同,如果是顺序查找,则从最小关键字从小到大进行查找。
图:
5、hash查找
Hash函数示例
代码如下:
#include "stdafx.h"
#include<iostream>
using namespace std;
const int INF=65535;
struct HashTable
{
int *elem;
int count;//存储HashTable中元素的个数
}H;
void InitHashTable(HashTable &H)//要动态数组存储元素
{
cout<<"请输入元素个数,以enter结束"<<endl;
cin>>H.count;
cout<<'\n';
H.elem=new int[H.count];
for(int i=0;i<H.count;i++)
H.elem[i]=INF;
}
void InsertHash(HashTable &H,int key)
{
int addr=key % H.count;//获取地址
while(H.elem[addr]!=INF)//判断是否冲突,即里面看有没有值
addr=(addr+1) % H.count;
H.elem[addr]=key;//找到空的就插入
}
void SearchHash(const HashTable &H,int key)
{
int addr=key % H.count;
int temp=addr;
while(H.elem[addr]!=key)
{
addr=(addr+1) % H.count;
if(addr==temp)//说明回到原点
{
cout<<"找不到这个关键字"<<endl;
return;
}
}
cout<<"关键字的位置是"<<addr<<endl;
}
int _tmain(int argc, _TCHAR* argv[])
{
InitHashTable(H);
int key;
cout<<"请输入"<<H.count<<"个Hash元素"<<endl;
for(int i=0;i<H.count;i++)
{
cin>>key;
InsertHash(H,key);
}
while(getchar()!='a')
{
cout<<"请输入要查找的Hash元素值"<<endl;
cin>>key;
SearchHash(H,key);
}
return 0;
}
输出结果是:
请输入元素个数,以enter结束
5
请输入5个Hash元素
1 2 4 20 84
请输入要查找的Hash元素值
1
关键字的位置是1
请输入要查找的Hash元素值
2
关键字的位置是2
请输入要查找的Hash元素值
4
关键字的位置是4
请输入要查找的Hash元素值
20
关键字的位置是0
请输入要查找的Hash元素值
84
关键字的位置是3
请输入要查找的Hash元素值
插入思想:
(1)相当于要将几个信息存在几块内存中,信息包括(学号,姓名)等。比如说这里有5个,那么有一个数组,或者说一个类型数组,利用Hash函数
求的地址。将其存储在这个数组类型中。
(2)当有冲突时,就用开放定址进行探测,获得其不冲突的地址,然后进行储存。
查找思想:
(1)当插入完毕之后,就可以根据学号,利用Hash函数进行查找了,同时,还可以找出对应的姓名。
(2)所以说还是有一定的实用价值的。
注,冲突的解决方法还有:
(1)二次探测法,其中d=1^2, -1^2 , 2^2 ,-2 ^2 ... ...q^2 (q<m/2) 。
(2)随机探测法,d为随机数。
(3)再散列函数法 ,取另外一个函数
(4)链地址法,将冲突以链表形式存储起来,散列表只存储头指针
(5)公共溢出区法,将冲突的放到溢出表中,查找时可以在里面进行顺序查找。
hash函数的构造方法:
直接定址法
数字分析法
随机数法
除留余数法
平方取中法
折叠法
总结:散列表对于那种查找性能要求高,记录之间无要求的数据有非常好的适用性。
6、算法时间复杂度再次总结
◆常数阶
◆线性阶
for(int i=0;i<n;i++)
◆对数阶
while(i<n)
i*=2;
也就是说2的x次方=n,计算后可以得到x=log2n;
◆平方阶
for(int i=0;i<n;i++)
for(int j=i;j<n;j++)
这里总共次数为n+(n-1)+(n-2)+..+1=n(n+1)/2
为平方阶
◆O(nlogn)
常见的时间复杂度:
O(1)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
从O(n3)开始之后的时间复杂度是不考虑的,不切实际
没有特别说明都指的是最坏时间复杂度。
7、二叉树的性质
度:节点子树个数。
深度:树的最大层数。
斜数
、满二叉树、完全二叉树
◆第i层上节点个数最多为2的i-1
◆第深度为k的二叉树,总节点不超过2的k次方-1
◆若叶子节点数目为n0,度为2的节点数为n2,则n0=n2+1;
通过完全二叉树可以看出来。
◆对于有n个节点的完全二叉树,其深度为[log2n]+1,其中[]为下闭合,为不超过
◆对于有n个节点的完全二叉树,若i>1,则其双亲为i/2,同样若2i=n,则其左节点为2i
8、二叉树的线索化
9、树与二叉树森林的转换
9、赫夫曼树及其应用
10、查找相关知识
1、有序表查找
二分查找思想:取中,若大则high-1,若小则low+1,相等则返回。
循环条件为:while(low<=high),计算次数一定要加上最后找到的那一次。
2、插值查找
斐波那契查找,根据斐波那契数列,进行黄金分割
3、线性索引查找
稠密索引、分块索引(块内无序,块间有序)、倒排索引
11、中缀表达式
符号的出栈形式:遇到由括号出栈符号,遇到相同等级或者优先级的符号则出栈,自己最后入栈
12、链表