《大话数据结构》第8章 查找--PART1

                            《大话数据结构》第8章 查找--PART1

    查找表(Search Table)是由同一类型的数据元素(或记录)构成的集合。

    关键字(Key)是数据元素中某个数据项的值,又称键值,用它可以标识一个数据元素。也可以标识一个记录的某个数据项(字段),我们称为关键码

    若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(Primary Key)。即对于不同的记录,其主关键字均不相同主关键字所在的数据项称为主关键码。

    对于那些可以识别多个数据元素的(或记录)的关键字,我们称为次关键字(Secondary Key),也可以理解为是不以唯一标识一个数据元素的关键字,对应的数据项就是次关键码。

    查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。若表中存在这样的一个记录,则称查找成功,此时查找的结果给出整个记录的信息,或指示该记录在查找表中的位置。若不存在,则称查找不成功,此时查找的结果可给出一个“空”记录或“空”指针。

    查找表按照操作方式分为两大种:静态查找表和动态查找表。

    静态查找表(Static Search Table):只作查找操作的查找表。主要操作有:

    (1)查询某个“特定的”数据元素是否在查找表中;

    (2)检索某个“特定的”数据元素和各种属性。

    动态查找表(Dynamic Search Table):在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素。其操作有:

    (1)查找时插入数据元素;

    (2)查找时删除数据元素。

1.顺序表查找

    顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个记录(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。

顺序表查找算法:

/*顺序查找,a为数组,n为要查找的数组长度,key为要查找的关键字*/
int Sequential_Search(int *a, int n, int key)
{
	int i;
	for (i = 0; i < n; i++)
	{
		if (a[i] == key)
			return i;
	}
	return -1;
}

顺序表查找优化:

    上述代码每次循环时都需要对i是否越界,即是否小于等于n作判断。事实上,还可以有更好一点的方法,设置一个哨兵,可以解决不需要每次让i与n作比较。改进代码如下:

/*有哨兵顺序查找*/
int Sequential_Search2(int *a, int n, int key)
{
	int i;
	a[0] = key;    //设置a[0]为关键字值,称之为哨兵
	i = n;         //循环从数组尾部开始
	while (a[i] != key)
	{
		i--;
	}
	return i;      //返回0说明查找失败
}

算法时间复杂度:

    最好:O(1),在第一个位置就查找成功

    最坏:O(n),在最后一个位置才查找成功

    平均:O(n)

优缺点:

    优--算法简单,对静态查找表的记录没有任何要求,在一些小型数据的查找时适用。

    缺--n很大时,查找效率低下。

2.有序表查找

2.1折半查找

    折半查找(Binary Search)技术,又称为二分查找。它的前提是线性表中的记录必须是关键码有序(通常由小到大有序),线性表必须采用顺序存储。基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。

折半查找算法:

/*折半查找*/
int Binary_Search(int *a, int n, int key)
{
	int low, high, mid;
	low = 0;                      //定义最低下标为记录首位
	high = n-1;                     //定义最高下标为记录末位
	while (low <= high)
	{
		mid = (low + high) / 2;     //折半
		if (key < a[mid])           //若查找值比中值小
		{
			high = mid - 1;         //最高下标调整到中位下标小一位
		}
		else if (key > a[mid])       //若查找值比中值大
		{
			low = mid + 1;          //最低下标调整到中位下标大一位
		}
		else
			return mid;            //若相等则说明mid即为查找到的位置
	}
	return -1;
}

算法时间复杂度:

    最好:O(1)

    最坏:O(logn)

    平均: O(logn)

优缺点:

    优--折半查找前提是需有序表顺序存储,对于静态查找表,一次排序后不再变化,这样的算法比较好了。

    缺--对于需要频繁执行插入或删除操作的数据集来说维护有序的排序会带来不小的工作量,不建议使用。

2.2插值查找

  首先考虑一个新问题,为什么一定要是折半,而不是折四分之一或者折更多呢?

  打个比方,在英文字典里面查“apple”,你下意识翻开字典是翻前面的书页还是后面的书页呢?如果再让你查“zoo”,你又怎么查?很显然,这里你绝对不会是从中间开始查起,而是有一定目的的往前或往后翻。

    同样的,比如要在取值范围1 ~ 10000 之间 100 个元素从小到大均匀分布的数组中查找5, 我们自然会考虑从数组下标较小的开始查找。

    经过以上分析,折半查找这种查找方式,还是有改进空间的,并不一定是折半的!

     mid = (low+high)/ 2, 即 mid = low + 1/2 * (high - low);

    改进为下面的计算机方案(不知道具体过程):mid = low + (key - a[low]) / (a[high] - a[low]) * (high - low),也就是将上述的比例参数1/2改进了,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。

插值查找算法:

#include <iostream>
using namespace std;
//需要有序数组,最好是均匀分布的。
int binary_search(int *a, int n, int key)
{
	int left ,right, mid;
        left = 0;
        right = n-1;
	while (left <= right)
	{
		mid = left + (int)(1.0*(key - A[left])) / (A[right] - A[left])*(right - left);	
		if (key<a[mid])
	            right = mid - 1;
		else if (key>a[mid])
		    left = mid + 1;
                else
                    return mid;
	}
	return -1;
}

int main()
{
	int a[8] = { 4, 7, 9, 12, 15, 34, 36, 67 };
	int key; cin >> key;
	cout << binary_search(a, 8, key);
	return 0;
}

    分析:从时间复杂度上来看,它也是O(logn),但是对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。

2.3斐波那契查找

原理:

    利用斐波那契数列的性质,黄金分割的原理来确定mid的位置。

    相对于二分查找和差值查找,斐波那契查找的实现略显复杂。但是在明白它的主体思想之后,掌握起来也并不太难。

    既然叫斐波那契查找,首先得弄明白什么是斐波那契数列。相信大家对这个著名的数列也并不陌生,无论是C语言的循环、递归,还是高数的数列,斐波那契数列都是一个重要的存在。而此处主要是用到了它的一条性质:前一个数除以相邻的后一个数,比值无限接近黄金分割

    就笔者而言,这种查找的精髓在于采用最接近查找长度的斐波那契数值来确定拆分点,初次接触的童鞋,请在读完下文后,自觉回过头来仔细体会这句话。举个例子来讲,现有长度为9的数组,要对它进行拆分,对应的斐波那契数列(长度先随便取,只要最大数大于9即可){1,1,2,3,5,8,13,21,34},不难发现,大于9且最接近9的斐波那契数值是f[6]=13,为了满足所谓的黄金分割,所以它的第一个拆分点应该就是f[6]的前一个值f[5]=8,即待查找数组array的第8个数,对应到下标就是array[7],依次类推。

    推演到一般情况,假设有待查找数组array[n]和斐波那契数组F[k],并且n满足n>F[k-1] && n <= F[k],则它的第一个拆分点middle=F[k]-1。

    这里得注意,如果n刚好等于F[k],待查找数组刚好拆成F[k-1]和F[k-2]两部分,那万事大吉你好我好;然而大多数情况并不能尽人意,n会小于F[k],这时候可以拆成完整F[k-1]和残疾的F[k-2]两部分,那怎么办呢?

    聪明的前辈们早已想好了解决办法,对了,就是补齐,用最大的数来填充F[k-2]的残缺部分,如果查找的位置落到补齐的部分,那就可以确定要找的那个数就是最后一个最大的了。

    low=mid+1说明待查找的元素在[mid+1,hign]范围内,k-=2 说明范围[mid+1,high]内的元素个数为 n - F[k-1]   =  F[k] - F[k-1] = F(k-2)个。

算法:

//斐波那契查找

#include <stdio.h>
#include <stdlib.h>
#define MAXN 20
 
//产生斐波那契数列

void Fibonacci(int *f)
{
	int i;
	f[0] = 0;
	f[1] = 1;
	for(i = 2;i < MAXN; ++i)
		f[i] = f[i - 2] + f[i - 1];
}
 
//查找

int Fibonacci_Search(int *a, int key, int n)
{
	int low = 0, high = n - 1, mid = 0;
	int i, k = 0;
	int F[MAXN];
	Fibonacci(F);
	while(n > F[k]) 			//计算出n在斐波那契中的数列
		++k;
	for(i = n; i < F[k]; ++i) //把数组补全
		a[i] = a[n-1];
	while(low <= high)
	{
		mid = low + F[k-1];  //根据斐波那契数列进行黄金分割
		if(a[mid] > key)
		{
			high = mid - 1;
			k = k - 1;
		}
		else if(a[mid] < key)
		{
			low = mid + 1;
			k = k - 2;
		}
		else
		{
			if(mid < n-1) //如果为真则找到相应的位置
				return mid;
			else
				return n-1;
		}
	}
	return -1;
}
 
int main()
{	
	int a[MAXN] = {5,15,19,20,25,31,38,41,45,49,52,55,57};
	int k, res = 0;
	printf("请输入要查找的数字:\n");
	scanf("%d", &k);
	res = Fibonacci_Search(a,k,13);
	if(res != -1)
		printf("在数组的第%d个位置找到元素:%d\n", res + 1, k);
	else
		printf("未在数组中找到元素:%d\n",k);
	return 0;
}
 

算法时间复杂度

    O(logn)

2.4总结

    折半查找进行加法与除法运算(mid = (low + high) / 2),插值查找进行复杂的四则运算( mid = low + (key - a[low] / (a[high] - a[low]) * (high - low)) ),斐波那契查找只是运用简单家减法运算 (mid  = low + f[k-1] -1) ,在海量的数据查找过程中,这种细微的差别会影响最终的查找效率。三种有序表的查找本质上是分割点的选择不同,各有优劣,实际开发可根据数据的特点综合考虑再做决定。

3.线性索引查找

    数据结构的最终目的是提高数据的处理速度,索引是为了加快查找速度而设计的一种数据结构。索引就是把一个关键字与它对应的记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少包含关键字和其对应的记录在存储器中的位置等信息。索引技术是组织大型数据库以及磁盘文件的一种重要技术。

    索引按照结构可以分为线性索引、树形索引和多级索引。我们这里只介绍线性索引技术。所谓线性索引就是将索引项集合组织为线性结构,也称为索引表。重点介绍三种线性索引:稠密索引、分块索引和倒排索引

3.1稠密索引

    稠密索引是指在线性索引中,将数据集中的每个记录对应一个索引项。如下图所示。稠密索引的索引项一定是按照关键码有序的排列。

    

    优点:索引项有序意味着,查找关键字时,可以用到折半、插值、斐波那契等有序查找算法,大大提高了效率。

    缺点:如果数据集非常大,比如上亿,那也就意味着索引也得同样的数据集长度规模,对于内存有限的计算机来说,可能就需要反复去访问磁盘,查找性能反而大大下降。

3.2分块索引

   分块有序-是把数据集的记录分成了若干份,并且这些块需要满足2个条件

    1)块内无序,即每一块内的记录不要求有序。(有序更好,但需要付出大量时间和空间代价);

    2)块间有序,例如要求第二块所有记录的关键字均要大于第一块所有记录的关键字,第三块的所有记录要大于第二块的所有记录关键字,因为只有块间有序,才能提高查找效率。

    定义的分块索引的索引项结构分为三个数据项

    (1)最大关键码,它存储每一块中的最大关键字,好处是可以使得它之后的下一块中的最小关键字也能比这一块最大的关键字要大;

    (2)块长,存储了块中的记录个数,以便于循环时使用;

    (3)块首指针,用于指向块首数据元素的指针,便于开始对这一块中记录遍历。

    时间复杂度:

    O(n)(顺序查找)<O(sqrt(n))<O(logn)(折半查找)

    分块索引在兼顾了对细分块不需有序的情况下,大大增加了整体查找的速度,所以普遍被用于数据库查找等技术的应用当中。

3.3倒排索引

    倒排索引的概念很简单:就是将文件中的单词作为关键字,然后建立单词与文件的映射关系。 当然,你还可以添加文件中单词出现的频数等信息。 倒排索引是搜索引擎中一个很基本的概念,几乎所有的搜索引擎都会使用到倒排索引。

    由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引。

    索引项的通用结构是:

    1)次关键码,例如上面的英文单词

    2) 记录号表,例如上面的文章编号

    其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。

4.二叉排序树

    二叉排序树(Binary Sort Tree),又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。

        (1)若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

        (2)若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

        (3)它的左右子树也分别为二叉排序树。

    注:当对二叉排序树进行中序遍历时,就可以得到一个有序的序列,如{8,10,11,12,13,15,16,17,18,19,22,25}。

    构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。不管怎么说,在一个有序数据集上的查找,速度总是要快于无序的数据集的,而二叉排序树这种非线性的结构,也有利于插入和删除的实现。

4.1 二叉排序树的建立、查找、插入和删除操作
 

二叉排序树删除操作

    对于二叉排序树中的节点A,对它的删除分为三种情况:

    (1)如果A为叶子结点,直接将A删除

    (2)如果A只有一个子节点(仅有左子树或右子树),就直接将A的子节点连至A的父节点上,并将A删除;

    (3)如果A有两个子节点,我们就以A的直接前驱或直接后继取代A,然后删除A。其中,A的前驱为A左子树中的最大值,即在左子树中一直向右走到底,A的后继为A右子树中的最小值,即在右子树中一直向左走到底。下图以直接后继取代待删除结点。

 


/*=========二叉树的二叉链表结点结构定义==========*/

typedef struct BiTNode                         //结点结构
{
	int data;                             //结点数据
	struct BiTNode *lchild, *rchild;      //左右孩子指针
}BiTNode, *BiTree;


/*================二叉排序树的建立===================*/




/*================二叉排序树的查找===================*/

BiTNode * SearchBST(BiTree &T,int key)  
{  
    BiTNode *p;  
    p=T;  
    if(T == NULL)  
        return NULL;  
    else if(p->key == key)  
        return p;  
    else if(p->key > key)  
        return SearchBST(p->lchild,key);  
    else  
        return SearchBST(p->rchild,key);  
}  


/*================二叉排序树的插入===================*/

/*
插入新元素时,可以从根节点开始,遇键值较大者就向左,遇键值较小者就向右,一直到末端,就是插入点
*/

void InsertBST(BiTree &T, int key)
{
	BiTree p = T, s;
	if (!SearchBST(p, key))          //查找不成功
	{
		s = (BiTree)malloc(sizeof(BiTNode));
		s->data = key;
		s->lchild = s->rchild = NULL;
		if (!p)                                  //若二叉树为空
			p = s;                              //插入s为新的根结点
		else if (key < p->data)
			p->lchild = s;                      //插入s为左孩子
		else
			p->rchild = s;                      //插入s为右孩子
		
	}
}



/*=======================二叉排序树的删除===============================*/
/*若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点*/


void DeleteBST(BiTree &T, int key)
{
	if (!T)        //不存在关键字等于key的数据元素
		return;
	else
	{
		if (key == T->data)     //找到关键字等于key的数据元素
			Delete(T);
		else if (key < T->data)    //
			DeleteBST(T->lchild, key);
		else
			DeleteBST(T->rchild, key);
	}
}

/*这段代码和前面的二叉排序树查找几乎完全相同,唯一地区别在于return Delete(T);*/

//从二叉排序树中删除结点p,并重接它的左或右子树
void Delete(BiTree &p)
{
	BiTree q, s;
	if (p->rchild == NULL)         //右子树空则只需重接它的左子树
	{
		q = p;
		p = p->lchild;
		free(q);
	}
	else if(p->lchild == NULL)     //左子树空则只需重接它的右子树
	{
		q = p;
		p = p->rchild;
		free(q);
	}
	else                             //左右子树均不空
	{
		q = p;
		//法1:以直接前驱取代待删除结点
		s = p->lchild;
		while (s->rchild)            //寻找待删结点p的左子树中最大值,即在左子树中一直向右走到底
		{
			q = s;
			s = s->rchild;
		}
		p->data = s->data;        //s指向被删除结点的直接前驱
		if (q != p)
			q->rchild == s->lchild;      //重接q的右子树
		else
			q->lchild == s->lchild;      //重接q的左子树
		
		//法2:以直接后继取代待删除结点
		s = p->rchild;
		while (s->lchild)
		{
			q = s;
			s = s->lchild;

		}
		p->data = s->data;
		if (q != p)
			q->lchild == s->rchild;      //重接q的左子树
		else
			q->rchild == s->rchild;      //重接q的右子树

		free(s);

	}
}


4.2 二叉排序树极端情况

    二叉排序树的优点在于保持了插入删除不用移动元素只要修改指针的优点。在查找上,查找次数等于待查找的结点在二叉排序树的层级。 极端情况最少为1次,即根结点就是要找的结点,最多也不会超过树的深度。

    来看一种极端情况:

    这种有序数组,查找最后一个结点99需要经历非常多的层级,其实查找次数还是偏多的。查找时间复杂度为O(N),等同于顺序查找。这样的情况下,树是不平衡的,右侧太重。

    我们为了提高二叉排序树的查找效率,需要把树构建得更为平衡,从而不出现左右偏重的情况。即其深度与完全二叉树相同,均为向下取整[log2N] +1,那么查找的时间复杂度为O(logN),近似于折半查找。

    这就引出了AVL树和红黑树这两种平衡二叉树了。

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值