当前位置: 首页 > news >正文

数据结构——查找

查找

1. 查找的基本概念

查找(Searching):就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素( 或记录)。查找结果分为两种,一种是查找成果,一种是查找失败。

查找表(Search Table):是由同一类型的数据元素(或记录)构成的集合。
关键字(Key):数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。例如,在由一个学生元素构成的数据集合中,学生元素中“学号”这一数据项的值唯一地标识一名学生。

静态查找表(Static Search Table):只作查找操作的查找表。主要操作有查询某个“特定的”数据元素是否在查找表中;检索某个“特定的”数据元素和各种属性。

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

平均查找长度:在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度,则是所有查找过程中进行关键字的比较次数的平均值,其数学定义为

A S L = ∑ i = 1 n P i C i ASL=\sum_{i=1}^nP_iC_i ASL=i=1nPiCi

式中, n n n是查找表的长度; P i P_i Pi是查找第 i i i个数据元素的概率,一般认为每个数据元素的查找概率相等,即 P i = 1 n P_i=\frac{1}{n} Pi=n1 C i C_i Ci是找到第 i i i个数据元素所需进行的比较次数。平均查找长度是衡量查找算法效率的最主要的指标。

2. 顺序查找和折半查找

2.1 顺序查找

2.1.1 顺序查找定义

顺序查找(Sequential Search) 又叫线性查找,是最基本的查找技术,作为一种最直观的查找方法,其基本思想是从线性表的一端开始,逐个检查关键字是否满足给定的条件。若查找到某个元素的关键字满足给定条件,则查找成功,返回该元素在线性表中的位置;若已经查找到表的另一端,但还没有查找到符合给定条件的元素,则返回查找失败的信息。

2.1.2 顺序查找算法

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

2.1.3 顺序查找效率

查找成功时的ASL,考虑 P i = 1 n P_i=\frac{1}n Pi=n1

A S L 成功 = 1 n ∑ i = 1 n ( n − i + 1 ) = n + 1 2 ASL_{成功}=\frac{1}{n}\sum_{i=1}^n(n-i+1)=\frac{n+1}{2} ASL成功=n1i=1n(ni+1)=2n+1

查找不成功比较 n + 1 n+1 n+1次,即:

A S L 不成功 = n + 1 ASL_{不成功}=n+1 ASL不成功=n+1

2.2 有序查找

2.2.1 有序查找定义

有序查找可以使用顺序查找,但在这里我们一般使用二分查找。

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

2.2.2 折半查找算法

int Binary_Search(SeqList L, ElemType key){
	int low = 0, high = L.length - 1, mid;
	while(low <= high){
		mid = (low + hight)/2;	//取中间位置
		if(L.elem[mid] == key){
			return mid;	//查找成功返回所在位置
		}else if(L.elem[mid] > key){
			high = mid - 1;	//从前半部分继续查找
		}else{
			low = mid + 1;	//从后半部分继续查找
		}
	}
	return -1;	//查找失败,返回-1
}

2.2.3 折半查找效率

下面举一个例子来看看二分查找的过程。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412082202615.png

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412082203831.png

二分查找的查找过程可以用一棵二叉树来描述。其中,树中的每个结点表示一个记录,结点中的值为该记录在表中的位置,通常称这个描述查找过程的二叉树为判定树。

以上面的例子建立判定树,显然这是一颗AVL树。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412082210509.png

由上述分析可知,用折半查找法查找到给定值的比较次数最多不会超过树的高度。在等概率
查找时,查找成功的平均查找长度为

A S L = 1 n ∑ i = 1 n l i = 1 n ( 1 × 1 + 2 × 2 + ⋯ + h × 2 h − 1 ) = n + 1 n l o g 2 ( n + 1 ) − 1 ≈ l o g 2 ( n + 1 ) − 1 \mathrm{ASL}=\frac{1}{n}\sum_{i=1}^{n}l_{i}=\frac{1}{n}(1\times1+2\times2+\cdots+h\times2^{h-1})=\frac{n+1}{n}\mathrm{log}_{2}(n+1)-1\approx\mathrm{log}_{2}(n+1)-1 ASL=n1i=1nli=n1(1×1+2×2++h×2h1)=nn+1log2(n+1)1log2(n+1)1

式中, h h h是树的高度,并且元素个数为 n n n时树高 h = ⌈ log ⁡ 2 ( n + 1 ) ⌉ h=\lceil\log_2(n+1)\rceil h=log2(n+1)⌉。所以,折半查找的时间复杂度 O ( l o g n ) O(logn) O(logn)

在上图所示的判定树中,查找成功的概率为 A S L = ( 1 × 1 + 2 × 2 + 3 × 4 + 4 × 4 ) / 11 = 3 ASL=(1\times1+2\times2+3\times4+4\times4)/11=3 ASL=(1×1+2×2+3×4+4×4)/11=3,即查找圆形结点;查找失败的概率为: A S L = ( 3 × 4 + 4 × 8 ) / 12 = 11 / 3 ASL=(3\times 4+4\times 8)/12=11/3 ASL=(3×4+4×8)/12=11/3,即查找方形结点。

2.3 分块查找

分块查找就是结合顺序查找和折半查找,如果数据被组织成分块有序表,将表分成几块,块内无序,块间有序,先确定待查记录所在块,再在块内查找。

分块查找的过程分为两步:第一步是在索引表中确定待查记录所在的块,可以顺序查找或折半查找索引表;第二步是在块内顺序查找。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412091634555.png

分块查找的平均查找长度为索引查找和块内查找的平均长度之和。设索引查找和块内查找的平均查找长度分别为 L I L_I LI L S L_S LS, 则分块查找的平均查找长度为

$$

ASL=L_1+L_{{S}}

$$

将长度为 n n n的查找表均匀地分为 b b b块, 每块有 s s s个记录, 在等概率情况下, 若在块内和索引表中均采用顺序查找,则平均查找长度为

$$

ASL=L_1+L_{{S}}=\frac{b+1}{2}+\frac{s+1}{2}=\frac{s^2+2 s+n}{2 s}

$$

此时, 若 s = n s=\sqrt{n} s=n , 则平均查找长度取最小值 n + 1 \sqrt n+1 n +1

若使用折半查找:

虽然索引表占用了额外的存储空间, 索引查找也增加了一定的系统开销, 但由于其分块结构,使得在块内查找时的范围较小, 因此与顺序查找相比, 分块查找的总体效率提升了不少。

A S L ≈ l o g 2 ( n s + 1 ) + s 2 ASL\approx log_2(\frac{n}s+1)+\frac{s} 2 ASLlog2(sn+1)+2s

3. 树形查找

3.1 二叉排序树(BST)

在前面我们已经讲过二叉排序树的定义,二叉排序树(也称二叉查找树)或者是一棵空树,或者是具有下列特性的二叉树:

  • 若左子树非空,则左子树上所有结点的值均小于根结点的值。
  • 若右子树非空,则右子树上所有结点的值均大于根结点的值。
  • 左、右子树也分别是一棵二叉排序树。

根据它的性质,我们可以看到,二叉排序树的中序遍历是有序序列。下面来介绍二叉排序树的操作函数。

  • 二叉排序树的查找

    二.叉排序树的查找是从根结点开始,沿某个分支逐层向下比较的过程。若二叉排序树非空,先将给定值与根结点的关键字比较,若相等,则查找成功:若不等,若小于根结点的关键字,则在根结点的左子树上查找,否则在根结点的右子树上查找。这显然是一个递归的过程。

    // 非递归写法
    BSTNode *BST_Search(BiTree T, ElemType key)
    {
        // 若树空或等于根结点值,则结束循环
        while (T != NULL && key != T->data)
        {
            // 小于,则在左子树上查找
            if (key < T->data)
                T = T->lchild; // 大于,则在右子树上查找
            else
                T = T->rchild;
        }
        return T;
    }
    
    // 递归写法
    BSTree SearchBST(BSTree T, char key)
    {
        // 在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素
        // 若查找成功,则返回指向该数据元素结点的指针,否则返回空指针
        if ((!T) || key == T->data.key)
            return T; // 查找结束
        else if (key < T->data.key)
            return SearchBST(T->lchild, key); // 在左子树中继续查找
        else
            return SearchBST(T->rchild, key); // 在右子树中继续查找
    } // SearchBST
    
  • 二叉排序树的插入

    插入与查找类似,都是利用二叉搜索树的特点,然后来找到合适的位置,最后当一个结点为空,证明找到了插入位置,代码如下,如果使用C++语言以及类来封装,会显得更加简便与易懂。返回值为节点,如果不使用返回值,则需要二级指针,这是从学习链表开始就一直提到的问题。

    https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412091912158.png

    BSTNode *BSTInsert(BSTNode *root, int key)
    {
        if (root == NULL) // 如果为空就插入
        {
            BSTNode *node = (BSTNode *)malloc(sizeof(BSTNode));
            node->left = node->right = NULL;
            node->val = key;
            return node;
        }
    
        if (key < root->val)
            root->left=BSTInsert(root->left,key);
        else
            root->right=BSTInsert(root->right, key);
        return root;
    }
    
  • 二叉排序树的删除

    二叉排序树的删除相对来说比较麻烦,因为需要考虑多种情况。通常分三种情况来处理。

    ①若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质。

    ②若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。

    ③若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。

    以下图为例:

    https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412091916526.png

BSTNode *findMin(BSTNode *root)
{
    while(root->left!=NULL)
    {
        root=root->right;
    }
    return root;
}
BSTNode *BSTDelete(BSTNode *root,int key)
{
    if(root==NULL)
        return root;
    // 首先要找到结点
    if(key<root->val)
        root->left=BSTDelete(root->left,key);
    else if(key>root->val)
        root->right=BSTDelete(root->right,key);
    else
    {
        // 找到后开始删除,考虑不同的情况
        if(root->left==NULL) // 只有左子树为空
        {
            BSTNode *temp=root->right;
            free(root);
            return temp;

        }else if(root->right==NULL) // 只有右子树
        {
            BSTNode *temp=root->left;
            free(root);
            return temp;
        }
        // 若有两个结点
        else
        {
            BSTNode *temp=findMin(root->right);// 找到右子树最小值
            root->val=temp->val;
            root->right=BSTDelete(root->right,temp->val);// 然后把问题转化成前面两种情况
        }
    }
    return root;

}

3.2 平衡二叉树

为了避免树的高度增长过快,降低二叉排序树的性能,规定在插入和删除结点时,要保证任意结点的左、右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树(Balanced Binary Tree),也称 AVL树。定义结点左子树与右子树的高度差为该结点的平衡因子,则平衡二叉树结点的平衡因子的值只可能是-1、0或1。

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412091949449.png

AVL的插入调整如下:

  • LL (右单旋转)麻烦节点在发现者左子树的左子树

    https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412091953337.png

  • RR (左单旋转)麻烦节点在发现者右子树的右子树

    https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412091954738.png

  • LR平衡双旋(先左后右双旋转)麻烦节点在发现者的左子树的右边

    https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412091958890.png

  • RL平衡双旋(先右后左双旋转)麻烦节点在发现者的右子树的左边

    https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412091959488.png

构造平衡二叉树的过程如下:

https://cdn.jsdelivr.net/gh/junmoxiao6661/pigo_image@main/202412091948487.png

相关文章:

  • 背包问题——多重背包(C语言)
  • [免费]直接整篇翻译pdf工具-支持多种语言
  • Unity导出WebGL
  • 【GPT入门】第23课 langchain serve介绍
  • 【网页】CSS原子化+变量尝试
  • test skills
  • openai 标准化协议 Structured Outputs 具体示例教程
  • Django settings.py 文件全解析
  • 案例:图书管理
  • 如何保证消息不被重复消费?(如何保证消息消费的幂等性)
  • golang-嵌套结构体
  • Unity利用噪声生成动态地形
  • vscode/windsurf/trae无法识别junit的@Test注解解决办法
  • C# WPF编程-启动新窗口
  • 新版AndroidStudio / IDEA上传项目到Gitee
  • 时间语义与窗口操作:Flink 流式计算的核心逻辑
  • Excel VBA实现智能合并重复元器件数据(型号去重+数量累加)
  • golang函数与方法的区别
  • 【组件安装】Ubuntu 22.04.5 desktop 安装 Anyware Agent
  • springboot441-基于SpringBoot的校园自助交易系统(源码+数据库+纯前后端分离+部署讲解等)
  • 安徽省公安厅原副厅长刘海石主动投案,正接受审查调查
  • 新经济与法|如何治理网购刷单与控评?数据合规管理是关键
  • 俄罗斯称已收复库尔斯克州,普京发表讲话
  • 最高法改判一起植物新品种侵权案:判赔逾5300万元破纪录
  • 美联储报告披露关税战冲击波:消费信心下降,经济担忧加深
  • 新华视点丨广西抗旱一线调查