从零开始_学_数据结构——查找算法索引二叉排序树

Posted qq20004604

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始_学_数据结构——查找算法索引二叉排序树相关的知识,希望对你有一定的参考价值。

查找算法

 

基本概念:

(1)关键字:假如有结构

struct Node //一个结点,存储数据和指针

{

DATA data; //数据属性,用于存储数据

int key; //假设key为int值,其在整个表里是唯一的

//指针域,具体略,指向其他结点,或者是数组的下标

};

key值便是关键字,对于每一个结点而言,其key值都是不一样的(不一定必须是int值)。因此,当我们查找数据时,只要知道其key值,然后对比key值和我们要查找的key值是否相同,便能判断是否是我们要查找的数据了。

优点:①数据属性的被更改,不影响查找,而且key值通常是不会被更改的;

缺点:①需要更多的空间用于存储key值;

 

2)查找时间复杂度:

查找时(查找也是一个算法),需要记录执行代码需要的时间(可以理解为执行多少行代码),而这个执行需要的时间,就是算法的时间复杂度。当数据量为n时,记作T(n)=Of(n)),f(n)表示数据量为n时的某个函数。

 

Of(n))指的是,这个函数在数据量为n时,算法的时间复杂度。

 

他表示随问题规模n的增大,算法执行的时间的增长率。这样用大写O()来体现算法复杂度的记法,称为大O记法。

 

一般关心的是大O记法的平均时间,和最坏结果的时间。

 

其常见的几种情况是:

①假如无论数据多少,其查找时间都是一个常数,那么记为O1),表示是常数;

②假如是对数型增长(数据每增长一倍,次数增加1次),记为Olog n),表示对数;

③假如是线性增长,记为On)。

④假如是乘方增长(和数据量的关系是乘方关系),记为On2)

 

3)查找表和查找

查找表:同一个类型的数据的集合(例如树中的结点是同一个类型,树中结点的集合就是一个查找表)。

查找:根据某个值(key),在查找表中确定一个项(比如树中的一个结点,比如说得到指向这个结点的指针)。

 

4)命中

可以理解为查找到自己要查找的项了。

 

顺序查找(线性查找):

(1)适用情况:

绝大多数情况。

 

2)原理:

从第一个开始查找,并依次尝试进行匹配,一直到查找到符合要求的值,或者是最后一个项为止。

 

(3)算法最佳适用情况:

①数据库较小;

②对时间没有苛刻要求。

 

4)算法时间复杂度:

On

 

 

二分法查找:

1)适用情况:

前提:key是有序的,并且表中顺序由key规定,且一般不变(如果变的话需要重新对表排序)。

 

(2)原理:

先找查找表中最中间的结点m,然后比较mkey值和要查找的key值的关系,如果比m.key值大,则找m右边的(范围比之前缩小一半)。如果比m.key值小,则找m左边的(范围依然比之前缩小一半)。如果和m.key值一样大,则命中。

如代码:

Node* find(int last, int key,Node *a)	//这里适用的是数组型,a指的是指向结点数组的指针
{
	int f, l, m;
	f = 0;
	l = last;
	while (f <= l)	//只要范围左限的下标比右限的下标小即可
	{
		m = (f + l) / 2;
		if (key < a[m].key)	//如果比中间结点的key值小
			l = m - 1;	//比中间值小1(也是出现新范围)
		else if (key>a[m].key)		//比中间结点的key值大
			f = m + 1;
		else
			return a;	//命中
	}
	return NULL;	//说明没命中,返回空指针
}

(2)算法最佳适用情况:

有序的表、有序的二叉树,数据较多时。

 

(3)算法时间复杂度:

Olog n

 

 

插值查找:

(1)算法原理:

根据key值和最大、最小下标之间的关系,来优化的。(具体我也没看懂)

 

(2)适用情况:

key值比较均匀的表。例如123456……这样。不适合分布极端的(如11005001000……)

 

(3)算法:

Node* find(int last, int key,Node *a)	//这里适用的是数组型,a指的是指向结点数组的指针
{
	int f, l, m;
	f = 0;
	l = last;
	while (f <= l)	//只要范围左限的下标比右限的下标小即可
	{
		m = l + (key - a[f].key) / (a[l].key - a[f].key)*(l - f);	//****修改的是这一行****
		if (key < a[m].key)	//如果比中间结点的key值小
			l = m - 1;	//比中间值小1(也是出现新范围)
		else if (key>a[m].key)		//比中间结点的key值大
			f = m + 1;
		else
			return a;	//命中
	}
	return NULL;	//说明没命中,返回空指针
}

 

(4)最佳适用情况:

key值分布均匀的。

 

 

斐波那契查找:

(1)原理:

没看懂。。。。好吧,是没耐心看。

 

2)适用情况:

对被查找值靠近右半侧的,效率比二分查找更高;

但对很靠近最左边的,效率比二分查找低。

 

 

 

 

索引:

所谓的索引,指有一个专门的索引表,用于存储每一个key值和指向该key值所在的项的指针。这里的索引,指的是 线性索引

 

例如:

技术分享


索引的特点:

①有序的。数据项可能是无序的(如图中的右半部分),但索引是有序的。因此无论数据项是否有序,只要找到key值符合的索引项,自然能根据索引项的指针,找到指向的数据项。

 

②因为有序(这里并非指索引表中全部有序),所以可以使用二分查找法,或者其他查找法,用于查找符合要求的key值。

 

③占用空间很小。可能只需要一个int值和一个指针,相比较数据项而言,空间小很多。

 

④但提升效率很高。在无序数据项中查找,基本只有线性查找了,然而利用索引,将线性索引(On)),变为二分法查找(Olog n)),因此提升效率很高。

 

 

稠密索引:

(1)定义:

指在线性索引中,数据集的每一个对应一个索引项。

 

(2)特点:

①一定是按有序(按key值)排列的;

②查找效率高;

 

(3)缺点:

①当数据量增长极快时,是没办法(或很难)进行有序排列的;

②当数据量极大时,读取是比较困难的(因为每一个对应一个索引项,因此索引表也很大)。

 

 

分块索引:

(1)定义:

将数据集分为了几块(几部分),然后每一块对应了一个索引项。

 

(2)特点:

①块内是无序的;

②块间是有序的(因此索引是有序的);

 

3)原理:

①块内虽然无序,但符合一定的要求,例如key值在一定范围之内;

②块内有两个值,用于记录块内的最小key值和最大key值;(因此其他块某项的值,必然比这个块所有项的key值大,或者小);

③索引项会有一个值,记录当前块内的项数;

④指针指向块首(无需知道块尾的,因为有项数,查找完项数的数目之后,自然结束);

 

4)优点:

①快速划定所在块,然后可以用逐个查找(此时块内项数并不多)也不会很慢。

 

 

倒排索引:

1)简单概念:

①设置关键词(这个关键词是我们要查找的内容,例如单词),然后产生一个关键词表。

②每个关键词项,有一个数组,记录该该关键词所在的项的编号(例如记录它是数据库里面的第几项);

③查找一个关键词时,可以先找关键词表中,该关键词所在的项。然后便找到该项记录的编号表(记录着有哪些数据库的项,有这个关键词);

④于是便得到一张表,里面每个项,都包含我们要找的关键词;

⑤显示出来,搜索结束。

 

2)优点:

①适合查找单词,原理简单,存储空间小,响应速度快。

②关键词表可以按首字母排列,然后同一个字母的所有单词甚至可以放到同一个块内(分块索引),因此效率很高;

③实际应用中,不需要一次显示出所有的,因此可以一次读取若干项(例如数组的0#~9#项,下一次再读取10#~19#项);

 

 

 

二叉排序树:

1)特点

利用二叉树的形式,进行搜索。

与二分搜索不同的是,二分搜索主要面对的是数组(有下标),而二叉排序树是没有数组的(用的是链表)。因此,在设计代码的时候,是不能用middle=first+last/2这样的办法的。

 

2)算法:(这里的二叉树换个思路重新写,新增删除结点

看的时候,建议自己画个二叉树,然后跟着代码思路走一遍,理解的会比较深刻。

结点:
struct Tree
{
	data m;
	int key;
	Tree* Lchild = NULL, *Rchild = NULL;
};
查找:
bool SearchTree(Tree*T, int key, Tree*p, Tree**n)	//指向当前结点的指针T,key值key,指向父结点p(默认为NULL),指向搜索路径上最后一个非空结点的指针的地址n
{
	if (T == NULL)	//如果是空指针(查找失败)
	{
		*n = p;		//指向当前结点指针的指针,指向父指针(实质上p是指向访问路径上的最后一个非空结点指针的地址)
		return false;
	}
	else if (T->key == key)	//当前结点的key值符合要求
	{
		*n = T;		//指向当前结点指针的指针,指向当前结点
		return true;
	}
	else if (T->key > key)	//在左子树
		SearchTree(T->Lchild, key, T, n);	//其参数分别为指向左孩子的指针,key值,指向左孩子的父结点的指针,指向
	else
		SearchTree(T->Rchild, key, T, n);
}
效果是,找到对象返回指向对象的指针,没有找到就返回空指针。

插入:
bool InsertTree(Tree*T, int key)	//先查找,key值重复插入失败返回false,key值不重复插入成功返回true
{
	Tree*temp = NULL, *p;
	if (!SearchTree(T, key, NULL, &temp))	//如果查找失败(说明不重复),此时temp的值是路径上最后一个非空结点(一定是一个叶结点)
	{
		p = new Tree;
		p->key = key;
		if (T == NULL)	//如果插入的位置是根结点(由于预先设置,因此T是存在的,直接赋值key给根结点的key
			T->key = key;
		if (p->key > temp->key)	//如果要插入的结点的key值比其父节点大
			temp->Rchild = p;
		else					//否则小(不可能相等)
			temp->Lchild = p;
		return true;	//插入成功,返回true
	}
	else
		return false;	//查找到,插入失败返回false
}
效果:将一个key值插入二叉树之中(不涉及数据域的操作),若二叉树里无该key值则成功,返回true,否则返回false

删除一个结点:

分为四种情况:

1)该结点为空结点(不用删);

2)该结点左子树为空,将指向自己的指针,指向自己的右子树;

3)该结点右子树为空,将指向自己的指针,指向自己的左子树;

4)该结点A左右子树都存在,在左子树(根结点为B)里找最右边的(或者在右子树找最左边的)结点C,然后替换自己。然后A的父结点指向A的指针,指向CC的左子结点指针指向BB的右子结点指向A的左子结点。

思路很简单,但是写起来很别扭。

我尝试写了一个,不确定是否正确。以后有机会的话在验证吧,如果有人验证出错,欢迎留言提醒。

bool DeleteTree(Tree**T, int key)	//先查找,然后删除
{
	Tree*l,*r;
	 if ((*T)->key = key)	//第一个就是(说明是根)
	{
		delete *T;
		return true;
	}
	else
	{
		while ((*T)->key != key&&(*T)!=NULL)	//如果当前key不同,并且不是空指针
		{
			l = (*T)->Lchild;
			r = (*T)->Rchild;
			if ((*T)->key > key)	//如果key更小
				*T = (*T)->Lchild;	//指向其左子
			else *T = (*T)->Rchild;	//如果key更大,指向其右子
		}
		if(*T==nullptr)	//如果是空指针,说明没找到
			return false;
		//于是此时找到key项了,并且此时l和r是其父结点的左右子
		Tree** temp;
		if (l->key == key)		//如果左子是key值
			temp = &l;	//temp是左指针的地址
		else temp = &r;	//否则temp是右指针的地址
		//于是*temp是指向该项的指针(其父节点指向其的指针)
		if ((*T)->Lchild == NULL)	//如果左子树为空
			*temp = (*T)->Rchild;
		else if ((*T)->Rchild == NULL)
			*temp = (*T)->Lchild;
		else		//否则左右子树都不空
		{
				//查找左子树的最右结点,及其父结点(因为要将修改他的右指针指向)
			Tree*A, *B, *C;
			C = (*temp)->Lchild;	//C将为其左子树的最右结点
			while (C->Rchild != NULL)
				C = C->Rchild;	//指向成功
			//4)该结点A左右子树都存在,在左子树(根结点为B)里找最右边的(或者在右子树找最左边的)结点C,然后替换自己。然后A的父结点指向A的指针,指向C,C的左子结点指针指向B,B的右子结点指向A的左子结点。
			A = *temp;	//A此时是指向被删除结点的指针
			*temp = C;	//被删除结点的父结点目前指向替换的结点
			B = A->Lchild;	//B是A的左子
			if (B != C)		//要排除B和C是同一个结点
			{
				B->Rchild = C->Lchild;	//B的左子是C的左子(左子树保持不变)
				C->Lchild = B;	//然后C的左子是B
			}
			C->Rchild = A->Rchild;	//C的右子是A的右子
			delete A;
		}
	}
}













以上是关于从零开始_学_数据结构——查找算法索引二叉排序树的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法查找(Search)详解

入门学算法_堆排序树遍历

《从零开始学Swift》学习笔记(Day 14)——字符串的插入删除和替换

算法与数据结构索引

2019.6.11_MySQL进阶一:索引

数据结构与算法学习笔记 查找