“ 忠诚、笃学、严谨、守纪 ”

点击任意处进入

数据结构笔记5

6 查找技术

6.1 概念

6.1.1 查找定义

  • 假设{K0,K1,K2,…,Kn-1}是互不相同的关键码,有一个包含n条记录的集合C,形式是(k0, R0),(k1, R1),(k2, R2),…, (kn-1,Rn-1),其中Rj是与关键码kj相关联的信息,0<=j<=n-1。

  • 给定某个关键码值K, 查找就是在C中定位记录(kj, Rj),使得kj=K。即查找,就是定位关键码值kj=K的记录的系统过程。

  • 查找算法的结果
    成功,就是至少找到一个关键码值为kj的记录,使得kj=K
    不成功,就是不存在记录可以使kj=K

6.1.2 查找算法的分类

  • 按查找之后,查找数据表是否改变分类

    1. 静态查找

      就是查找算法执行之后,无论是否找到记录( kj,Rj )使得kj=K ,并不改变备查找的数据表。

    2. 静态查找
      就是查找算法执行之后,无论是否找到记录( kj,Rj )使得kj=K ,并不改变备查找的数据表。

  • 按记录组织形式分类

    基于线性表的查找。如顺序查找、折半查找

    根据关键码值直接访问。如用数组下标直接查找、散列查找

    树索引查找方法。如二叉搜索树、B树等

    基于属性的查找方法。如倒排表、倒排文件等

6.2 线性表的查找技术

6.2.1 折半查找

折半查找(Binary Search):当存储表上的数据关键字有序,那么每次可以缩小近乎一半的查找范围,直到查找成功,或不成功。

(05 13 19 21 37 56 64 75 80 88 92)

每次key与mid比较,大于mid指定的值,则移动low和mid;小于mid指定的值,则移动high和mid;直到low>high,则表示查找不成功。

  • 折半算法——递归思想

    假设有n个从小到大排序的元素,现在需要查找元素x。
    在[1..n]范围内容,假设l=1,h=n,用x与a[n/2]比较;
    如果x=a[n/2],则找到x,算法中止;
    如果x<a[n/2],则在 [1..n/2-1](即 l=1,h=n/2-1)范围内继续搜索x,就是设n=n/2-1,重复第1步;
    如果x>a[n/2],则在[n/2+1..n] (即 l=n/2+1,h=n)范围内继续搜索x,就是设只要在数组a的后半部分重复第1步搜索x。

    // 折半查找前提数据元素已排序,查找关键码为x的元素
    int BinSearch(List  list, KeyType  x, int *position)
    { int low, mid, high;
      low=0; high=list->n-1;
      while(low<=high)
       {  mid=(low+high)/2;
           if (list->element[mid].key == x)
              { *position=mid; return 1; }//查找成功
           else  if(list->element[mid].key > x)
                              high=mid-1;//查前半部分
                 else  low=mid+1;//查后半部分
       }
       *position=low; return 0;//查找失败
    }
    

    or

// 折半查找前提数组元素data已排序,查找x
int BinSearch(DataType  data[],  DataType  x,  int nArrayLen)
{ int nMid = nArrayLen / 2;
   if ( data[nMid]==x )   
            return nMid; // 找到x
   if (nMid == 0)    return -1; // 找不到
   if (x < data[nMid])
          return BinSearch(data, x, nMid);//查前半部分
   else
          return BinSearch(data+nMid+1,  x, nArrayLen – nMid-1);//查后半部分
}
  • 折半查找的时间效率:1780446160008.png

折半查找只能在顺序存储结构的线性表上实现

6.2.2 链表上实现的静态和动态查找

  • 带头结点链表静态查找

    1780446372875.png
  • 带头结点链表动态查找

    PNode Dyn_Search(LinkList llist, DataType x)
    { PNode p=llist, q, s;//q指向比较点,p指向q的前一点
    q=p->next;
    while (q){//查找
        if (q->data==x) return q;//找到
        if (q->data>x) break; //没找到
        p=q; q=q->next; //下移一位,这里表示要插在p,q之间
    }
    s=(PNode)malloc(sizeof(struct Node));//插入新结点
    s->data=x;
    s->next=p->next;  p->next=s; return s;//链入链表
    }
    

6.3 二叉树在数据动态查找中的应用

二叉搜索树/二叉排序树

6.3.1 定义

  • 一棵二叉排序树是一棵二叉树,它可以为空。如果不为空,则它将满足

    1. 非空左子树的所有键值小于其根结点的键值(关键字)

    2. 非空右子树的所有键值大于其根结点的键值

    3. 左、右子树都是二叉排序树

1780446571975.png

6.3.2 二叉排序树查找算法

  • 静态查找

从根结点开始比较,直到数值被找到,或没找到

int search (PBinSearchTree ptree, KeyType key,
                      PBinSearchNode *position) {
    PBinSearchNode p,q;
    p=*ptree; q=p;
    while (p!=NULL) {  q=p;
      if (p->key==key) { *position=p; return 1; //找到 }
      else  if (p->key > key) p=p->Left; else p=p->Right; }
       *position=q; return 0; //没找到
}
  • 动态查找(!!!)

在二叉排序树上插入元素,一定是插在叶子结点上,第一个结点一定是根节点

int insert (PBinSearchTree ptree, keyType key){
  PBinSearchNode p, position; //p插入点,position插入位置
  if (search(ptree, key, &position)==1) return 1; //找到
  p=(PBinSearchTree )malloc(sizeof(struct BinSearchNode));
  if(p==NULL) { printf(“\Error\n”); return 0; }
  p->key=key; p->Left=p->Right=NULL;
  if (position==NULL) *ptree=p; //找到插入位置
  else if (key<position->key) position->Left=p; //插入左子树
           else position->Right=p; //插入右子树
  return 1;
}

#include <stdio.h>
#include <stdlib.h>

// 1. 定义【关键字类型】 这里用int举例
typedef int KeyType;

// 2. 定义【二叉排序树节点结构】
// 每个节点包含:数据 + 左孩子指针 + 右孩子指针
struct BinSearchNode {
    KeyType key;                // 节点存储的数据(关键字)
    struct BinSearchNode *Left; // 左孩子指针(存比我小的数)
    struct BinSearchNode *Right;// 右孩子指针(存比我大的数)
};

// 3. 给节点指针、树根指针起别名,方便书写
// PBinSearchNode = 节点指针
// PBinSearchTree = 树根指针(二级指针,因为要修改根节点)
typedef struct BinSearchNode *PBinSearchNode;
typedef struct BinSearchNode **PBinSearchTree;

// ===================== 核心函数1:查找 =====================
// 功能:在树中查找key
// 参数:
//   ptree    树根指针(二级指针)
//   key      要查找的值
//   position 传出参数:找到=目标节点,没找到=最后停留的节点(插入用)
// 返回值:1=找到  0=没找到
int search(PBinSearchTree ptree, KeyType key, PBinSearchNode *position) {
    PBinSearchNode p, q;

    p = *ptree;  // p 从根节点开始遍历
    q = p;       // q 永远保存 p 的上一个节点(父节点)

    // 循环遍历:p走到空就结束
    while (p != NULL) {
        q = p;  // q 跟上 p,保存当前节点

        // 情况1:找到了!
        if (p->key == key) {
            *position = p;  // 把目标节点返回给调用者
            return 1;
        }
        // 情况2:要找的值 < 当前节点 → 去左子树
        else if (p->key > key) {
            p = p->Left;
        }
        // 情况3:要找的值 > 当前节点 → 去右子树
        else {
            p = p->Right;
        }
    }

    // 循环结束 p=NULL → 没找到
    // position = q(最后停留的父节点,插入时用)
    *position = q;
    return 0;
}

// ===================== 核心函数2:插入 =====================
// 功能:向二叉排序树插入一个key
// 返回值:1=插入成功  0=失败/已存在
int insert(PBinSearchTree ptree, KeyType key) {
    PBinSearchNode p, position;

    // 第一步:先查找!
    // 如果返回1 → 节点已存在,不插入,直接返回
    if (search(ptree, key, &position) == 1) {
        printf("关键字 %d 已存在,插入失败!\n", key);
        return 1;
    }

    // 第二步:没找到 → 创建新节点
    p = (PBinSearchNode)malloc(sizeof(struct BinSearchNode));
    if (p == NULL) {
        printf("内存分配失败!\n");
        return 0;
    }
    // 初始化新节点
    p->key = key;
    p->Left = NULL;
    p->Right = NULL;

    // 第三步:把新节点挂到树上
    // 情况1:position=NULL → 树是空树 → 新节点当根
    if (position == NULL) {
        *ptree = p;
    }
    // 情况2:key < 父节点 → 插左边
    else if (key < position->key) {
        position->Left = p;
    }
    // 情况3:key > 父节点 → 插右边
    else {
        position->Right = p;
    }

    printf("关键字 %d 插入成功!\n", key);
    return 1;
}

// ===================== 辅助函数:中序遍历 =====================
// 二叉排序树【中序遍历】一定是 从小到大有序序列!
void inOrder(PBinSearchNode tree) {
    if (tree != NULL) {
        inOrder(tree->Left);   // 遍历左子树
        printf("%d ", tree->key); // 访问根节点
        inOrder(tree->Right);  // 遍历右子树
    }
}

// ===================== 主函数:测试示例 =====================
int main() {
    // 定义一棵空树(树根初始为NULL)
    PBinSearchNode tree = NULL;

    // 插入测试数据
    insert(&tree, 50);
    insert(&tree, 30);
    insert(&tree, 80);
    insert(&tree, 20);
    insert(&tree, 40);
    insert(&tree, 90);
    insert(&tree, 50); // 重复值,插入失败

    printf("\n中序遍历结果(从小到大有序):");
    inOrder(tree);
    printf("\n");

    return 0;
}

连续调用实现创建:

int createSearchTree(PBinSearchTree ptree, 
                           SeqDictionary *dic){
    int i;
    *ptree=NULL;
    for (i=0; i<dic->n; i++)
          if (!insert(ptree, dic->element[i].key))
                 return 0; //插入不成功
    return 1; //插入成功
}

练习1:依次输入元素10,8,16,5,20,7,12,19,试生成一棵二叉排序树,画出建立的二叉排序树

练习2:现在对二叉排序树进行动态查找数据{36,15,24,13,9}

  • 删除某个元素

    int delete(PBinSearchTree ptree, KeyType key);

    • 第1种情况,删除叶子结点

      直接删除叶结点

    • 第2种情况,删除单枝结点
      直接删除结点,且单枝孩子树直接上升

    • 第3种情况,删除双枝结点
      找左子树上最大值(或者是右子树上最小值)替换删除结点值,将问题转为第2种情况

1780449193684.png

6.3.3 二叉排序树性能分析

对于有 n 个关键码的集合,其关键码有 n! 种不同排列,可构成不同二叉搜索树有

1780449656034.png(棵)

  • 二叉排序树的特点分析

    它是一棵二叉树
    关于结点的值有:左子树< 根< 右子树
    定义是递归的: 左子树和右子树依然是二叉排序树

  • 二叉排序树的动态查找算法

    查找: 从根往下走O(h)
    插入: 查找失败后O(1),总O(h)
    删除: 分情况讨论, O(h)

退化:斜二叉树

6.4 平衡二叉树(判断)

6.4.1 定义

  • 平衡二叉树(AVL树),或者是空树,或者是具有下列性质的非空二叉排序树

    1. 任一结点的左、右子树均为AVL树

    2. 任一结点的左、右子树高度差的绝对值不超过1

6.4.2 平衡因子(BF, Balance Factor)

  • 某结点的左子树与右子树的高度差即为该结点的平衡因子

  • 平衡二叉树上所有结点的平衡因子只可能是 -1,0 或 1

BF(T)=hL-hR

引入平衡二叉树的目的是为了提高查找效率, 使其平均查找长度为O(log2n)

6.4.3 平衡二叉树的构造

如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡,称调整平衡过程为平衡旋转

平衡旋转可以归纳为四类(调平衡不考)

  1. LL平衡旋转

    若在A的左子树的左子树上插入结点,使A的平衡因子从1变为2,需要进行一次顺时针旋转。(以B为旋转轴)

  2. RR平衡旋转

    若在A的右子树的右子树上插入结点,使A的平衡因子从-1增加至-2,需要进行一次逆时针旋转。
    (以B为旋转轴)

  3. LR平衡旋转

    若在A的左子树的右子树上插入结点,使A的平衡因子从1变为2,需要先进行逆时针旋转,再顺时针旋转。 (以插入的结点C为旋转轴)

  4. RL平衡旋转

    若在A的右子树的左子树上插入结点,使A的平衡因子从-1变为-2,需要先进行顺时针旋转,再逆时针旋转。 (以插入的结点C为旋转轴)

6.3 散列查找

  • 常见查找算法的分析
分类 算法 平均查找长度
静态查找表 顺序查找 1780450882196.png
折半查找 1780450909067.png
动态查找表 二叉排序树 O(log(n))
二叉平衡树 O(log(n))

以上讨论的查找表的各种结构的共同特点:

  1. 记录在表中的位置和它的关键字之间不存在一个确定的关系

  2. 查找的效率取决于和给定值进行比较的关键字个数

  3. 用这类方法表示的查找表,其平均查找长度都不为零

有没有ASL = 0的查找表?

6.3.1 散列表(哈希表)

实际问题求解

学校为招收的新生(大约500名左右)建立查找表,设关键字为学号,其值的范围为 xxxxxx00 ~ xxxxxx99 (前4位为年份,再2位是班级号)

下标为00 ~99 是顺序号,如果把它作为顺序表下标

则查找班级内某个学生过程可以简化:取给定值(学号)的后2位,不需要经过比较便可直接从顺序表中找到待查关键字

散列查找的概念

散列法,杂凑法,关键码-地址转换法

基本思想:根据一个关键码,用一个函数h(key)计算出该记录元素的存储地址,并尽量将记录存储到期望的地址

eg:有学号yyyyxzhh,yyyy入学年份,x专业,z班号,hh顺序号。设计学生信息存储顺序表下标函数为h(hh),试问20142213学生存储在班级顺序表第几位?

  • 散列表(哈希表)的定义

    根据设定的散列函数(也称哈希函数) H(key) 和所选中的处理冲突的方法,将一组关键字映象到一个有限的、地址连续的地址集 (区间) 上,并以关键字在地址集中的“象”作为相应记录在表中的存储位置,如此构造所得的查找表称之为散列表(Hash Table,即哈希表)

1780626683667.png
  • 冲突与同义词

    如果两个不相等的关键码key1和key2,用散列函数 f 计算得到相同的散列地址(即f(key1)=f(key2)),称为冲突(碰撞),称key1和key2为同义词

    负载因子:又称装填因子

1780626778087.png

  • 实现散列查找的方法
  1. 需在关键字与记录在表中的存储位置之间建立一个函数关系,以 h(key) 作为关键字为 key 的记录在表中的位置,通常称这个函数 h(key) 为哈希函数

  2. 必须设计一种解决冲突的方法,以保证散列算法得于实现

  • Hash函数的特点

    (1) 哈希(Hash)函数是一个映象,即将关键字的集合映射到某个地址集合上,它的设置很灵活,只要这个地址集合的大小不超出允许范围即可

    (2) 由于哈希函数是一个压缩映象,因此,在一般情况下,很容易产生“冲突”现象,即 key1 key2,而 h(key1) = h(key2)

    (3) 很难找到一个不产生冲突的哈希函数。一般情况下,只能选择恰当的哈希函数,使冲突尽可能少地产生

哈希函数的构造方法

  • 数字分析法

    假设关键字集合中的每个关键字都是由 s 位数字组成 (u1, u2, …, us),分析全体关键字, 并从中提取分布均匀的若干位或它们的组合作为地址

    1781047717319.png

    1781047747633.png

此方法适合于
事先明确知道表中所有关键字每一位数值的分布情况,它完全依赖于关键字集合。如果换一个关键字集合,选择哪几位要重新决定

把超出地址位数的最高位删去, 仅保留最低的3位,作为可用的散列地址

  • 折叠法

    此方法把关键码自左到右分成位数相等的几部分, 每一部分的位数应与散列表地址位数相同, 只有最后一部分的位数可以短一些
    把这些部分的数据叠加起来, 就可以得到具有该关键码的记录的哈希地址
    有两种叠加方法

    • 移位叠加 — 把各部分的最后一位对齐相加

    • 间界叠加 — 各部分不折断, 沿各部分的分界来回折叠, 然后对齐相加, 将相加的结果当做哈希地址

    例 设给定的关键码为 key = 23938587841, 若存储空间限定3位,则划分结果为每段3位,上述关键码可划分为4段
    239 385 878 41

    1780627356742.png

    此方法适合于 关键字的数字位数特别多,而且关键字中每一位上数字分布大致均匀时

  • 平方取中法

    以关键字的平方值的中间几位作为存储地址。求“关键字的平方值” 的目的是“扩大差别”,同时平方值的中间各位又能受到整个关键字中各位的影响,先计算构成关键码的标识符内码的平方, 再按表的大小取中间的若干位作为哈希地址

1780627211266.png

  • 直接定址法

    哈希函数为关键字的线性函数
    H(key) = key 或者            H(key) = a  key + b

    这类哈希函数是一对一的映射,一般不会产生冲突

    此法仅适合于
    地址集合的大小 = = 关键字集合的大小

  • 除留余数法

    设定哈希函数为 H(key) = key MOD p 其中,p≤m (表长)

1780627433806.png

一般情况下,若散列表表长为m,通常可以选p为小于或等于表长(最好接近m)的最大质数或不包含小于20质因子的合数。

  • 随机数法

    设定哈希函数为

    H(key) = Random(key)

    其中,Random 为伪随机函数

    通常,此方法用于对长度不等的关键字构造哈希函数

  • 非数字关键字的常见Hash函数

    1. ASCII码加和法

    2. 移位法

实际造哈希表时,采用何种构造哈希函数的方法取决于建表的关键字集合的情况(包括关键字的范围和形态),总的原则是使产生冲突的可能性降到尽可能地小。

练习:一组长度为11的整型关键字为{11,21,12,34,43,45,54,65,67,78,89},通过哈希函数H(key)= key Mod 11映射到长度为11的哈希表中。画出哈希表,并放入数据,然后计算装填因子。

6.3.2 处理冲突的方法

“处理冲突” 的实际含义是为产生冲突的地址寻找下一个哈希地址

  • 开放定址法

    为产生冲突的地址 H(key) 求得一个地址序列

    H0, H1, H2, …, Hs 1≤ s≤m-1

    其中 H0 = H(key)

    Hi = ( H(key) + di ) MOD m
    i=1, 2, …, s

    对增量 di 有三种取法

    • 线性探测再散列 di = c i 最简单的情况 c=1

    • 平方探测再散列 di = 12, -12, 22, -22, …,

    • 随机探测再散列 di 是一组伪随机数列

1781048080029.png

练习1 :假设哈希函数为H(key)= k MOD 7,用线性探测法处理冲突。试在0~8的散列地址空间中对关键字序列(100,20,21,35,3,78,99,45)构造哈希表,并求等概率下查找成功时的平均查找长度 。

  • 再哈希法

    Hi=RHi(key) i=1,2,3,…,k

    RHi均是不同的哈希函数,即在同义词产生地址冲突时计算另一个哈希函数地址,直到冲突不再发生。

  • 分离链接法

    将所有哈希地址相同的记录都链接在同一链表中。即所有的关键字为同义词的记录都在同一线性链表上。

    1781048201459.png

练习2:设哈希函数为H(key)= k MOD 13,用分离链接法处理冲突。试在0~12的散列地址空间中对关键字序列(240,29,345,189,100,20,21,35,3,208,78,99,45,350)构造哈希表,并求等概率下查找成功时的平均查找长度 。

  • 建立一个公共溢出区

    基本思想:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素一律填入溢出表。

6.3.3 哈希表的存储结构

  • 哈希表存储就是一个顺序存储的线性表

    //结点结构的定义
    typedef int KeyType;
    typedef int DataType;
    typedef struct {
        KeyType key;
        DataType value;
    }Element;
    

    //哈希表的定义
    typedef struct {
    Element *element;
    int m;
    }Hash, *pHash;

- 构建一个空的线性表

```c
 pHash creatEmpty(int m){
     pHash phd=(pHash)malloc(sizeof(pHash));
     if (phd!=NULL){
          phd->element=(Element *)mallc(sizeof(Element)*m);
          if (phd->element){ 
             phd->m=m;
             for(int i=0; i<m; i++) phd->element[i].key=0; //初始化
             return (phd);       }
     else free(phd);  }
     printf(“Out of space!\n”);
     return NULL;
  }
  • 散列表的查找

    //假设用线性探测法解决冲突
        int HashSearch (Hash *phash, KeyType key, int *position) {
            int d, inc;
            d=h(key); //假设散列函数是h(key)
            for (inc=0;  inc<phash->m;  inc++) {
                 if (phash->element[d].key==key) { //找到
                       *position=d;   return1; }
                 else if (phash->element[d].key==0) {
                             *position=d; return 0;} //没有找到,确认插入位置
                 d=(d+1) % phash->m; //线性探测
            } //for
            *position=-1; //没找到,也没确认插入位置,即溢出
            return  0;
        }
    
  • 散列表数据的插入

    int HashInsert(Hash  *phash, Element ele){
      int position;
      if (HashSearch( phash, ele.key, &position)==1))
          printf(“查找成功!\n”);
      else if (position!=-1)
                   phash->element[position]=ele; //插入结点
               else return 0; //溢出
      return 1;
    }
    

6.3.4 哈希表查找分析

  • 查找过程

    给定Key值,根据哈希函数求得哈希地址,若表中此位置的值和给定Key相等,则查找次数为1;否则说明产生冲突,根据处理冲突的方法找“下一个地址”,直到找到为止,记录下查找的次数

    1781048395036.png

    1781048419929.png

从查找过程得知,哈希表查找的平均查找长度实际上并不等于零

  • 决定哈希表查找的ASL的因素

    • 选用的哈希函数

    • 选用的处理冲突的方法

    • 哈希表饱和的程度,即装填因子α=n/m 值的大小(n—记录数,m—表的长度),装填因子越大,说明表越饱和,冲突的概率越高

一般情况下,可以认为选用的哈希函数是“均匀”的,则在讨论ASL时,可以不考虑它的因素。

因此,哈希表的ASL是处理冲突方法和装填因子的函数。

1781048530050.png

从以上结果可见,哈希表的平均查找长度是  的函数,而不是 n 的函数。

这说明,用哈希表构造查找表时,可以选择一个适当的装填(负载)因子  ,使得平均查找长度限定在某个范围内。

posted @ 2026-06-03 09:47  alonep  阅读(20)  评论(0)    收藏  举报