数据结构笔记之----查找

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的概念及用途,添加删除节点的原理。

B树的功能是提高了此判断额IO性能,但是没有解决元素遍历效率很低的问题,其思路是将数据分散到所有的结点中,如查找一个数,根据范围去找到指针,去磁盘找到数据块,再读取,不断去找,IO性能提高,但是对于范围性的查找,就需要遍历整个B树,效率较低。
B+树对B树进行了扩展,其B+树的数据都集中在叶结点,上层结点只是数据的索引,并不包含数据信息。这样当进行范围性查找的时候,只需要从叶子节点遍历即可。因此一般的关系型数据库都喜欢用B+树做索引,但是也有用红黑树做索引的。

多路查找树:一个节点的孩子数可以多于两个,且一个节点处可以存储多个元素。

(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、算法时间复杂度再次总结

方法:
1、用常数1取代操作
2、只保留最高阶项
3、去除与最高阶相乘的常数

◆常数阶

◆线性阶

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、二叉树的线索化

其作用就是将左右空子树分别指向其前驱和后继。
为了区分是指向前驱还是左节点,用一个lTag为0或者1来标识
因此其结构为:data lchild rchild lTag rTag
线索化方法:
节点构造函数中,默认将标识设为0,
对于每一个节点,处理好自己指向前驱,处理好前驱指向自己。
采用中序遍历,若左子树为空,则设置左标识为1,指向前一个节点,若前节点右子树为空,则将其前节点的后继指向它,设置前驱标识。最后将当前节点作为参数传入遍历右子树的函数中。

9、树与二叉树森林的转换

◆将树转化成二叉树
加线,兄弟之间加线。
去线,只保留与第一个孩子节点的连线。
层次调整,和原来的比较,自己的第一个孩子为自己的左孩子,若原来为兄弟,现在为孩子,则是右孩子。
◆森林转换为二叉树
将每棵树转化成二叉树,在作为根节点连接。

9、赫夫曼树及其应用

其方法是:
(1)先将原来的按照大小顺序排列,然后取前两个组成新值,注意小的在左边,并将新的节点和原来剩下的排序。
(2)重复步骤1.
(3)左子树取0右子树取1即可。
带权路径长度:a*长度+b*长度。。。
注意可能有些题中,会出现大的在左边,小的在右边或者换方向,但是不影响wpl的情况。

10、查找相关知识

1、有序表查找

二分查找思想:取中,若大则high-1,若小则low+1,相等则返回。

循环条件为:while(low<=high),计算次数一定要加上最后找到的那一次。

2、插值查找

斐波那契查找,根据斐波那契数列,进行黄金分割

3、线性索引查找

稠密索引、分块索引(块内无序,块间有序)、倒排索引

11、中缀表达式

符号的出栈形式:遇到由括号出栈符号,遇到相同等级或者优先级的符号则出栈,自己最后入栈

12、链表

注意单向链表以及双向链表都是右头结点的
而单向循环链表就是将最后的指向了前面,同样如果是双向的话,前面的还要指向后面。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值