数据结构算法-----查找的那些事


在这里插入图片描述

查找的基本概念

查找表:是一种以同一类型的记录构成的集合为逻辑结构,以查找为核心运算的数据结构

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

主关键字:可以惟一地标识一个记录的关键字。对于那些可以标识多个数据元素(或记录)的关键字,称为次关键字

查找:是在由一组记录组成的集合中寻找关键字值等于给定值的某个记录,或是寻找属性值符合特定条件的某些记录

动态查找表和静态查找表:
  静态:只作查找操作的查找表
  动态:动态表的特点是表结构本身是在查找过程中动态生成的。同时在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素

平均查找长度:为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度

线性表的查找

1.顺序查找:
	a.查找过程:
		从表的一端开始,依次将记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功,反之。查找失败。
	b.适用:
		线性表的顺序存储结构,也适用于线性表的链式存储结构。
2.折半查找:
	a.查找过程:
		从表的中间记录开始,如果给定值和中间记录的关键字相等,则查找成功;如果给定值大于或者小于中间记录的关键字,则在表中大于或小于中间记录的那一半中查找,这样重复操作,直到查找成功,或者在某一步中查找区间为空,则代表查找失败
	b.适用:
		只是适用于有序表,且限于顺序存储结构(线性链表无法进行折半查找)
3.分块查找:
	a.查找过程:
		第一步在索引表中确定待查记录所在的块,可以顺序查找或者折半查找索引表;第二步在块内顺序查找。

顺序查找算法:

template<class ElemType> int SqSearch(ElemType elem[],int n,ElemType key)
//一般顺序查找比较
{
    
    
    int i;
    for(i=0; i<n && elem[i]!=key ;i++);
    if(i<n)
        return i;
    else
        return -1;
}
template<class ElemType> int SqSearch(ElemType elem[],int n)
//使用哨兵elem[0],n的传入的是带上哨兵的长度
{
    
    
    int i;
    for(i=n;elem[i]!=elem[0];i--);
    if(i==0)
        return -1;
    else
        return i;
}

注意:
  1.哨兵优化是对顺序查找的优化,因为每次循环时都需要对i是否越界(i是否小于等于n)作判断。设置一个哨兵,可以解决越界问题。对于查找数字比较大的情况,哨兵的优点更加明显。
  2.如果数据元素的数据类型是一个结构体的话,需要在结构体中重载不等于(!=)关系运算。

折半查找算法:

//递归算法
template<class ElemType> int BinSearch(ElemType elem[],int low,int high,ElemType key)
{
    
    
    int mid;
    if(low>high)
        mid=-1;//查找失败
    else
    {
    
    
        mid=(low+high)/2;
        if(key<elem[mid])//左半边继续查找
            mid=BinSearch(elem,low,mid-1,key);
        else if(key>elem[mid])//右半边继续查找
            mid=BinSearch(elem,mid+1,high,key);
        //两个条件都不满足或者已经调用过,mid就是最后的结果
    }
    return mid;
}
//非递归算法
template<class ElemType> int BinSearch(ElemType elem[],int n,ElemType key)
{
    
    
    int low=0,high=n-1;//设置查找到的左右边界
    int mid;
    while(low<=high)
    {
    
    
        mid=(low+high)/2;
        if(key==elem[mid])
            return mid;
        else if(key<elem[mid])
            high=mid-1;
        else
            low=mid+1;
    }
    return -1;
}

注意:折半查找的算法思想

  1)如果查找区间长度小于1(low>high)则表示查找失败,返回-1;否则继续以下步骤。

  2)求出查找区间中间位置的数据元素下标 mid(mid=(low+high)/2)。

  3)用区间中间位置的数据元素的关键字elem[mid]与给定值key进行比较,比较的结果有以下三种可能。
    ①若elem[mid]=key,则查找成功,报告成功信息并返回其下标mid。
    ②若elem[mid]<key,则说明如果数据表中存在要找的数据元素,该数据元素一定在mid的右侧,可把查找区间缩小到数据表的后半部分(low=mid+1),再继续进行折半查找(转步骤1)。
    ③若elem[mid]>key,则说明如果数据表中存在要找的数据元素,该数据元素一定在mid的左侧。可把查找区间缩小到数据表的前半部分(high=mid-1),再维续进行折半查找(转步骤1)。

  在折半查找过程中,每比较一次,如果数据元素的关键字和给定值不相等,则查找区间缩小一半。直到查找区间已缩小到只有一个数据元素,如果仍未找到想要找的数据元素,则表示查找失败。

三种查找方法的比较:

  a.顺序查找的优点是算法简单,且对表的存储结构无任何要求.缺点是当n较大时,查找效率低
  b.二分查找的速度快,效率高,适用于表不易变动且又经常查找的情况
  c.分块查找是在表中插入或删除一个记录时,只要找到该记录所属的块,可以在该块进行运算,不适宜用链式存储结构

树表的查找

二叉排序树:

1.二叉排序树的定义

   二叉排序树(简称BST)又称二叉查找(搜索)树,其定义为:二叉排序树或者是空树,或者是满足如下性质的二叉树:

  (1)若它的左子树非空,则左子树上所有记录的值均小于根记录的值;
  (2)若它的右子树非空,则右子树上所有记录的值均大于根记录的值;
  (3)左、右子树本身又各是一棵二叉排序树。

2.二叉排序树的查找

  因为二叉排序树可看做是一个有序表,所以在二叉排序树上进行查找,和二分查找类似,也是一个逐步缩小查找范围的过程。

  递归查找算法SearchBST()如下(在二叉排序树bt上查找关键字为k的记录,成功时返回该节点指针,否则返回NULL):

   BSTNode *SearchBST(BSTNode *bt,KeyType k)
 {
    
      if (bt==NULL || bt->key==k)        //递归终结条件
       return bt;
    if (k<bt->key)
      return SearchBST(bt->lchild,k); //在左子树中递归查找
    else
      return SearchBST(bt->rchild,k); //在右子树中递归查找
 }

也可以采用如下非递归算法:

BSTNode *SearchBST1(BSTNode *bt,KeyType k)
{
    
      while (bt!=NULL)
   {
    
    
      if (k==bt->key)
          return bt;
      else if (k<bt->key)
          bt=bt->lchild;  //在左子树中递归查找
      else
          bt=bt->rchild;  //在左子树中递归查找
   }
   else                   //没有找到返回NULL
      return NULL;
 }

3.二叉排序树的插入

  在二叉排序树中插入一个关键字为k的新记录,要保证插入后仍满足BST性质。

插入过程

  (1)若二叉排序树T为空,则创建一个key域为k的节点,将它作为根节点;
  (2)否则将k和根节点的关键字比较,若两者相等,则说明树中已有此关键字k,无须插入,直接返回0;

  (3)若k 小于T->key,则将k插入根节点的左子树中。

  (4)否则将它插入右子树中。

对应的递归算法InsertBST()如下:

int InsertBST(BSTNode *&p,KeyType k) 
//在以*p为根节点的BST中插入一个关键字为k的节点。插入成功返回1,否则返回0
{
    
      if (p==NULL)  //原树为空, 新插入的记录为根节点
   {
    
       p=(BSTNode *)malloc(sizeof(BSTNode));
       p->key=k;p->lchild=p->rchild=NULL;
       return 1;
   }
   else if  (k==p->key) //存在相同关键字的节点,返回0
      return 0;
   else if (k<p->key) 
      return InsertBST(p->lchild,k); //插入到左子树中
   else  
      return InsertBST(p->rchild,k);  //插入到右子树中

 }

4.二叉排序树的删除

  (1)被删除的节点是叶子节点:直接删去该节点。
  (2)被删除的节点只有左子树或者只有右子树,用其左子树或者右子树代替它。
  (3)被删除的节点既有左子树,也有右子树:以其前驱替代之,然后再删除该前驱节点。前驱是左子树中最大的节点。也可以用其后继替代之,然后再删除该后继节点。后继是右子树中最小的节点。

平衡二叉树(AVL):

1.平衡二叉树的定义

  若一棵二叉树中每个节点的左、右子树的高度至多相差1,则称此二叉树为平衡二叉树。

  在算法中,通过平衡因子(balancd factor,用bf表示)来具体实现上述平衡二叉树的定义。

  平衡因子:平衡二叉树中每个节点有一个平衡因子域,每个节点的平衡因子是该节点左子树的高度减去右子树的高度。从平衡因子的角度可以说,若一棵二叉树中所有节点的平衡因子的绝对值小于或等于1,即平衡因子取值为1、0或-1,则该二叉树称为平衡二叉树。
在这里插入图片描述

散列表的查找

1.散列表的基本概念
   a.散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。建立了关键字与存储位置的映射关系,公式如下:存储位置 = f(关键字)
   这里把这种对应关系f称为散列函数,又称为哈希(Hash)函数。
 
  b.采用散列技术将记录存在在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。那么,关键字对应的记录存储位置称为散列地址。

  c.散列技术既是一种存储方法也是一种查找方法。散列技术的记录之间不存在什么逻辑关系,它只与关键字有关,因此,散列主要是面向查找的存储结构。
2.散列函数的构造方法

2.1 直接地址法: 所谓直接定址法就是说,取关键字的某个线性函数值为散列地址

  • 优点:简单、均匀,也不会产生冲突。
  • 缺点:需要事先知道关键字的分布情况,适合查找表较小且连续的情况。

使用:由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用。

2.2 数字分析法: 如果关键字时位数较多的数字,比如11位的手机号"130****1234",其中前三位是接入号;中间四位是HLR识别号,表示用户号的归属地;后四为才是真正的用户号。

  使用:数字分析法通过适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布比较均匀,就可以考虑用这个方法。

2.3 折叠法: 是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

  使用:折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。

2.4 平方取中法: 这个方法计算很简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做散列地址。

  使用:平方取中法比较适合不知道关键字的分布,而位数又不是很大的情况。

2.5 除留余数法: 此方法为最常用的构造散列函数方法。这方法不仅可以对关键字直接取模,也可以再折叠、平方取中后再取模。

  使用:本方法的关键在于选择合适的p,p如果选不好,就可能会容易产生冲突。若散列表的表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
3.处理冲突的方法

3.1 概念: 在理想的情况下,每一个关键字,通过散列函数计算出来的地址都是不一样的,可现实中,这只是一个理想。市场会碰到两个关键字key1 != key2,但是却有f(key1) = f(key2),这种现象称为冲突出现冲突将会造成查找错误,因此可以通过精心设计散列函数让冲突尽可能的少,但是不能完全避免。

3.2 开放地址法: 所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

3.3 链地址法: 将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表前面的指针。对于关键字集合{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},用前面同样的12为余数,进行除留余数法。
      此时,已经不存在什么冲突换地址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保证。当然,这也就带来了查找时需要遍历单链表的性能损耗。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/S_yyuan/article/details/122636307