C/C++ 数据结构与算法【查找】 线性表查找、树表的查找、散列表的查找详细解析【日常学习,考研必备】带图+详细代码

一、查找的基本概念

1)在哪里找?

2)什么查找?

3)查找成功与否?

4)查找的目的是什么?

5)查找表怎么分类?

6)如何评价查找算法?

7)查找过程中我们要研究什么?

查找的方法取决于查找表的结构,即表中数据元素是依何种关系组织在一起的。

由于对查找表来说,在集合中查询或检索一个"特定的"数据元素时若无规律可循,只能对集合中的元素--加以辨认直至找到为止。

而这样的"查询"或"检索"是任何计算机应用系统中使用频度都很高的操作,因此设法提高查找表的查找效率,是本章讨论问题的出发点。

为提高查找效率,一个办法就是在构造查找表时,在集合中的数据元素之间人为地加上某种确定的约束关系。

研究查找表的各种组织方法及其查找的过程的实施。

二、线性表的查找

1)顺序查找(线性查找)


c 复制代码
int Search_Seq(SSTable ST, KeyType key) {
	//若成功返回其位置信息,否则返回0
	for (i = ST.length; i >= 1; --i) {
		if (ST.R[i].key == key) {
			return i;
		}
		return 0;
	}
}

算法7.1其他形式

c 复制代码
int Search_Seq(SSTable ST, KeyType key) {
	for (i = ST.length; S.TR[i].key != key; --i)
		if (i <= 0) break;
	if (i > 0) return i;
	else return 0;
}
c 复制代码
int Search_Seq(SSTable ST, KeyType key) {
	for (i = ST.length; ST.R[i].key != key && i > 0; --i);
	if (i > 0) return i;
	else return 0;
}

上述代码每执行一次循环都要进行两次比较,是否能够改进?

**改进:**把待查关键字key存入表头("哨兵"、"监视哨"),可免去查找过程中每一步都要检测是否查找完毕,加快速度。

设置监视哨的顺序查找:

c 复制代码
int Search Seq( SSTable ST ,KeyType key ){
    ST.R[0].key = key;
    for(i = ST.length; ST.R[i].key != key; --i );
    return i;
}                                    

当 ST.Iength 较大时,此改进能使进行一次查找所需的平均时间几乎减少一半。

1、主要代码

c 复制代码
#include <stdio.h>
#include <corecrt_malloc.h>

#define KeyType int // 假设关键字类型为整型

typedef struct {
    KeyType key; // 数据元素的关键字
} ElemType;

typedef struct {
    ElemType* elem; // 指向数据元素的指针
    int length;     // 查找表中数据元素的数量
} SSTable;

// 顺序查找函数,返回目标值在SSTable中的位置(从1开始),如果没有找到则返回0
int Search_Seq(SSTable ST, KeyType key) {
    // 将要查找的关键字作为"监视哨"放在数组的第一个位置
    ST.elem[0].key = key;

    for (int i = ST.length; i >= 1; --i) {
        if (ST.elem[i].key == key) {
            return i; // 返回找到的位置
        }
    }
    return 0; // 如果没有找到,返回0表示查找失败
}

// 示例main函数用于测试Search_Seq函数
int main() {
    // 初始化一个简单的查找表
    SSTable ST;
    ST.length = 5;
    ST.elem = (ElemType*)malloc((ST.length + 1) * sizeof(ElemType));

    // 填充查找表的数据
    int keys[] = { 8, 3, 7, 9, 2 };
    for (int i = 1; i <= ST.length; ++i) {
        ST.elem[i].key = keys[i - 1];
    }

    // 测试查找函数
    KeyType searchKey = 7;
    int position = Search_Seq(ST, searchKey);
    if (position != 0) {
        printf("找到了! 索引为 %d.\n", position);
    }
    else {
        printf("未找到该数字.\n");
    }

    free(ST.elem); // 清理动态分配的内存
    return 0;
}

2、算法分析

时间复杂度:O(n)

查找成功时的平均查找长度, 设表中各记录查找概率相等ASLs(n) = (1+2+ ... +n)/n = (n+1)/2

空间复杂度:一个辅助空间------O(1);

3、讨论

1、记录的査找概率不相等时如何提高査找效率?

查找表存储记录原则------按查找概率高低存储

  • 查找概率越高,比较次数越少
  • 查找概率越低,比较次数较多

2、记录的查找概率无法测定时如何提高查找效率?

方法------按香找概率动态调整记录顺序:

  • 在每个记录中设一个访问频度域
  • 始终保持记录按非递增有序的次序排列
  • 每次查找后均将刚查到的记录直接移至表头

4、特点

优点:算法简单,逻辑次序无要求,且不同存储结构均适用

缺点:ASL太长,时间效率太低。

2)折半查找(二分或对分查找)


1、非递归算法

  • 设表长为 n,low、high 和 mid 分别指向待查元素所在区间的上界、下界和中点,key为给定的要查找的值:

  • 初始时,令 low = 1,high = n ,mid = 「 (low+high)/2 」

  • 让k与mid指向的记录比较

    • 若 key == R[mid].key 查找成功

    • 若 key < R[mid].key,则 high = mid - 1

    • 若 key > R[mid].key,则 low = mid + 1

  • 重复上述操作,直至low > high 时,查找失败

c 复制代码
int Search_Bin(SSTable ST, KeyType key) {
	low = 1; 
	high = ST.length;// 置区间初值
	while (low <= high) {
		mid = (low + high) / 2;
		if (ST.R[mid].key == key) return mid; // 找到待査元素
		else if (key < ST.R[mid].key) // 缩小查找区间
			high = mid - 1;    // 继续在前半区间进行查找
		else low = mid + 1;    // 继续在后半区间进行査找
	}
	return 0; // 顺序表中不存在待查元素
}// Search Bin 

2、主要代码

c 复制代码
#include <stdio.h>
#include <corecrt_malloc.h>

#define KeyType int // 假设关键字类型为整型

typedef struct {
    KeyType key; // 数据元素的关键字
} ElemType;

typedef struct {
    ElemType* R; // 指向数据元素的指针(假设从索引1开始存储有效数据)
    int length;  // 查找表中数据元素的数量
} SSTable;

// 折半查找函数,返回目标值在SSTable中的位置(从1开始),如果没有找到则返回0
int Search_Bin(SSTable ST, KeyType key) {
    int low = 1;
    int high = ST.length; // 置区间初值

    while (low <= high) {
        int mid = low + (high - low) / 2; // 防止(low + high)溢出

        if (ST.R[mid].key == key) {
            return mid; // 找到待查元素
        }
        else if (key < ST.R[mid].key) {
            high = mid - 1; // 继续在前半区间进行查找
        }
        else {
            low = mid + 1;  // 继续在后半区间进行查找
        }
    }

    return 0; // 顺序表中不存在待查元素
}

// 示例main函数用于测试Search_Bin函数
int main() {
    // 初始化一个简单的查找表
    SSTable ST;
    ST.length = 5;
    ST.R = (ElemType*)malloc((ST.length + 1) * sizeof(ElemType));

    // 填充查找表的数据,并确保数据已排序
    int sortedKeys[] = { 2, 3, 7, 8, 9 };
    for (int i = 1; i <= ST.length; ++i) {
        ST.R[i].key = sortedKeys[i - 1];
    }

    // 测试查找函数
    KeyType searchKey = 7;
    int position = Search_Bin(ST, searchKey);
    if (position != 0) {
        printf("找到了! 索引为 %d.\n", position);
    }
    else {
        printf("未找到该数字.\n");
    }

    free(ST.R); // 清理动态分配的内存
    return 0;
}

3、递归算法

c 复制代码
// 折半查找的递归函数,返回目标值在SSTable中的位置(从1开始),如果没有找到则返回0
int Search_Bin_Recursive(SSTable ST, KeyType key, int low, int high) {
    if (low > high) {
        return 0; // 查找不到时返回0
    }

    int mid = low + (high - low) / 2; // 防止(low + high)溢出

    if (key == ST.elem[mid].key) {
        return mid; // 找到待查元素
    } else if (key < ST.elem[mid].key) {
        // 递归,在前半区间进行查找
        return Search_Bin_Recursive(ST, key, low, mid - 1);
    } else {
        // 递归,在后半区间进行查找
        return Search_Bin_Recursive(ST, key, mid + 1, high);
    }
}

4、主要代码

c 复制代码
#include <stdio.h>
#include <corecrt_malloc.h>

#define KeyType int // 假设关键字类型为整型

typedef struct {
    KeyType key; // 数据元素的关键字
} ElemType;

typedef struct {
    ElemType* elem; // 指向数据元素的指针(假设从索引1开始存储有效数据)
    int length;     // 查找表中数据元素的数量
} SSTable;

// 折半查找的递归函数,返回目标值在SSTable中的位置(从1开始),如果没有找到则返回0
int Search_Bin_Recursive(SSTable ST, KeyType key, int low, int high) {
    if (low > high) {
        return 0; // 查找不到时返回0
    }

    int mid = low + (high - low) / 2; // 防止(low + high)溢出

    if (key == ST.elem[mid].key) {
        return mid; // 找到待查元素
    }
    else if (key < ST.elem[mid].key) {
        // 递归,在前半区间进行查找
        return Search_Bin_Recursive(ST, key, low, mid - 1);
    }
    else {
        // 递归,在后半区间进行查找
        return Search_Bin_Recursive(ST, key, mid + 1, high);
    }
}

// 示例main函数用于测试Search_Bin_Recursive函数
int main() {
    // 初始化一个简单的查找表
    SSTable ST;
    ST.length = 5;
    ST.elem = (ElemType*)malloc((ST.length + 1) * sizeof(ElemType));

    // 填充查找表的数据,并确保数据已排序
    int sortedKeys[] = { 2, 3, 7, 8, 9 };
    for (int i = 1; i <= ST.length; ++i) {
        ST.elem[i].key = sortedKeys[i - 1];
    }

    // 测试查找函数
    KeyType searchKey = 7;
    int position = Search_Bin_Recursive(ST, searchKey, 1, ST.length);
    if (position != 0) {
        printf("找到了! 索引为 %d.\n", position);
    }
    else {
        printf("未找到该数字.\n");
    }

    free(ST.elem); // 清理动态分配的内存
    return 0;
}

5、算法分析

判定树



6、特点

优点:效率比顺序查找高

缺点:只适用于有序表,且限于顺序存储结构(对线性链表无效)

3)分块查找(索引顺序查找或跳跃查找)

1、主要代码

c 复制代码
#include <stdio.h>
#include <stdlib.h>

#define KeyType int // 假设关键字类型为整型

typedef struct {
    KeyType key; // 数据元素的关键字
} ElemType;

typedef struct {
    ElemType *elem; // 指向数据元素的指针(假设从索引1开始存储有效数据)
    int length;     // 查找表中数据元素的数量
} SSTable;

typedef struct {
    int blockStartIndex; // 每个块起始位置的索引
    KeyType maxKey;      // 每个块的最大关键字
} IndexBlock;

// 初始化分块查找结构
void Init_Block_Index(SSTable ST, IndexBlock index[], int blockSize) {
    int i, j;
    for (i = 0; i <= ST.length / blockSize; ++i) {
        index[i].blockStartIndex = i * blockSize + 1;
        index[i].maxKey = -1; // 假设所有关键字都是正数
        
        for (j = 0; j < blockSize && index[i].blockStartIndex + j <= ST.length; ++j) {
            if (ST.elem[index[i].blockStartIndex + j].key > index[i].maxKey) {
                index[i].maxKey = ST.elem[index[i].blockStartIndex + j].key;
            }
        }
        
        if (index[i].maxKey == -1) { // 如果没有更多元素则停止初始化
            break;
        }
    }
}

// 分块查找函数,返回目标值在SSTable中的位置(从1开始),如果没有找到则返回0
int Search_Block(SSTable ST, IndexBlock index[], int blockSize, KeyType key) {
    int i = 0;
    
    // 在索引表中查找目标块
    while (i < ST.length / blockSize && key > index[i].maxKey) {
        ++i;
    }
    
    // 如果找到了合适的块,则在该块内进行顺序查找
    if (i < ST.length / blockSize) {
        int start = index[i].blockStartIndex;
        int end = start + blockSize - 1;
        end = (end > ST.length) ? ST.length : end; // 确保不越界
        
        for (int j = start; j <= end; ++j) {
            if (ST.elem[j].key == key) {
                return j; // 找到待查元素
            }
        }
    }
    
    return 0; // 未找到该数字
}

// 示例main函数用于测试Search_Block函数
int main() {
    // 初始化一个简单的查找表
    SSTable ST;
    ST.length = 12;
    ST.elem = (ElemType *)malloc((ST.length + 1) * sizeof(ElemType));
    
    // 填充查找表的数据,并确保块间有序
    int keys[] = {5, 8, 3, 7, 9, 6, 1, 4, 2, 10, 12, 11};
    for (int i = 1; i <= ST.length; ++i) {
        ST.elem[i].key = keys[i - 1];
    }

    // 定义块大小
    int blockSize = 4;
    IndexBlock index[(ST.length / blockSize) + 1];

    // 初始化分块索引
    Init_Block_Index(ST, index, blockSize);

    // 测试查找函数
    KeyType searchKey = 7;
    int position = Search_Block(ST, index, blockSize, searchKey);
    if (position != 0) {
        printf("找到了! 索引为 %d.\n", position);
    } else {
        printf("未找到该数字.\n");
    }

    free(ST.elem); // 清理动态分配的内存
    return 0;
}

2、算法分析

总共元素个数 n ,每个块内元素个数 s,(例如:上述总元素个数18个,块内元素6个)

3、优缺点

优点:插入和删除比较容易,无需进行大量移动。

缺点:要增加一个索引表的存储空间并对初始索引表进行排序运算。

适用情况:如果线性表既要快速查找又经常动态变化,则可采用分块查找。

4)三者比较

三、树表的查找

当表插入、删除操作频繁时,为维护表的有序性,需要移动表中很多记录。

  • 改用动态查找表------几种特殊的树表结构在查找过程中动态生成
  • 对于给定值key若表中存在,则成功返回;否则,插入关键字等于key 的记录
二叉排序树
平衡二叉树
红黑树
B- 树
B+ 树
键树

1)二叉排序树

二叉排序树(Binary Sort Tree)又称为二叉搜索树、二叉查找树

定义:

二叉排序树或是空树或是满足如下性质的二叉树:

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

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

(3)其左右子树自身又各是一棵二叉排序树

1、主要代码

存储结构

c 复制代码
typedef struct {
	KeyType key; // 关键字项
	InfoType otherinfo; // 其他数据域
}ElemType;

typedef struct BSTNode {
	ElemType data;  // 数据域
	struct BSTNode* lchild, * rchild;// 左右孩子指针
}BSTNode,*BSTree;

BSTree T;//定义二叉排序树T
(1)查找

【算法思想】

(1)若二叉排序树为空,则查找失败,返回空指针。

(2)若二叉排序树非空,将给定值key与根结点的关键字

T -> data.key进行比较:

  • 若key 等于 T -> data.key,则查找成功,返回根结点地址,
  • 若key 小于 T -> data.key,则进一步査找左子树;
  • 若key 大于 T -> data.key,则进一步查找右子树。
c 复制代码
BSTree SearchBST(BSTree T, KeyType 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
(2)插入

若二叉排序树为空,则插入结点作为根结点插入到空树中

否则,继续在其左、右子树上查找

  • 树中已有,不再插入
  • 树中没有
    • 查找直至某个叶子结点的左子树或右子树为空为止,则插入结点应为该叶子结点的左孩子或右孩子

插入的元素一定在叶结点上

c 复制代码
// 插入操作:将新元素插入到合适的位置,保持二叉排序树的性质
BSTree InsertBST(BSTree* T, ElemType e) {
    if (*T == NULL) { // 如果是空树或到达叶子节点
        *T = (BSTNode*)malloc(sizeof(BSTNode));
        (*T)->data = e;
        (*T)->lchild = (*T)->rchild = NULL;
        return *T;
    }

    if (e.key < (*T)->data.key)
        return InsertBST(&(*T)->lchild, e); // 在左子树中插入
    else if (e.key > (*T)->data.key)
        return InsertBST(&(*T)->rchild, e); // 在右子树中插入
    // 关键字已存在,不进行插入

    return *T;
}
(3)生成

一个无序序列可通过构造二叉排序树而变成一个有序序列。

构造树的过程就是对无序序列进行排序的过程。

插入的结点均为叶子结点,故无需移动其他结点。相当于在有序序列上插入记录而无需移动其他记录。

但是:

关键字的输入顺序不同建立的不同二叉排序树

(4)删除

​ 从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去...只能删掉该结点,并且还应保证删除后所得的二又树仍然满足二叉排序树的性质不变。

​ 由于中序遍历二又排序树可以得到一个递增有序的序列。那么,在叉排序树中删去一个结点相当于删去有序序列中的一个结点。

  • 将因删除结点而断开的二叉链表重新链接起来
  • 防止重新链接后树的高度增加
被删除结点是叶子结点


被删除结点只有左子树和右子树

其双亲结点的相应指针域的值改为 " 指向被删除结点的左子树或右子树 "

被删除的既有左子树又有右子树





c 复制代码
// 找到并返回最小值节点
BSTNode* FindMin(BSTree T) {
    if (T == NULL) return NULL;
    while (T->lchild != NULL) T = T->lchild;
    return T;
}

// 删除操作:从二叉排序树中删除指定关键字的节点,并调整树结构以保持其性质
BSTree DeleteBST(BSTree* T, KeyType key) {
    if (*T == NULL) {
        return NULL; // 未找到关键码为key的结点
    }
    else if (key < (*T)->data.key) {
        (*T)->lchild = DeleteBST(&(*T)->lchild, key); // 在左子树中删除
    }
    else if (key > (*T)->data.key) {
        (*T)->rchild = DeleteBST(&(*T)->rchild, key); // 在右子树中删除
    }
    else { // 找到要删除的结点
        BSTNode* p = *T;

        if ((*T)->lchild == NULL) { // 情况1: 只有右子树或没有子树
            *T = (*T)->rchild;
            free(p);
        }
        else if ((*T)->rchild == NULL) { // 情况2: 只有左子树
            *T = (*T)->lchild;
            free(p);
        }
        else { // 情况3: 有两个孩子节点
            // 找到右子树中的最小节点
            BSTNode* minNode = FindMin((*T)->rchild);
            (*T)->data = minNode->data; // 用最小节点的数据替换当前节点的数据
            (*T)->rchild = DeleteBST(&(*T)->rchild, minNode->data.key); // 递归删除最小节点
        }
    }
    return *T;
}
(5)总体代码
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义关键字类型和其他信息类型
typedef int KeyType;
typedef char InfoType[50]; // 假设其他信息为字符串

// 定义数据元素类型
typedef struct {
    KeyType key;        // 关键字项
    InfoType otherinfo; // 其他数据域
} ElemType;

// 定义二叉排序树节点结构体
typedef struct BSTNode {
    ElemType data;  // 数据域
    struct BSTNode* lchild, * rchild; // 左右孩子指针
} BSTNode, * BSTree;

// 查找操作:根据给定的关键字查找对应的节点
BSTree SearchBST(BSTree T, KeyType 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);  // 在右子树中继续查找
}

// 插入操作:将新元素插入到合适的位置,保持二叉排序树的性质
BSTree InsertBST(BSTree* T, ElemType e) {
    if (*T == NULL) { // 如果是空树或到达叶子节点
        *T = (BSTNode*)malloc(sizeof(BSTNode));
        (*T)->data = e;
        (*T)->lchild = (*T)->rchild = NULL;
        return *T;
    }

    if (e.key < (*T)->data.key)
        return InsertBST(&(*T)->lchild, e); // 在左子树中插入
    else if (e.key > (*T)->data.key)
        return InsertBST(&(*T)->rchild, e); // 在右子树中插入
    // 关键字已存在,不进行插入

    return *T;
}

// 找到并返回最小值节点
BSTNode* FindMin(BSTree T) {
    if (T == NULL) return NULL;
    while (T->lchild != NULL) T = T->lchild;
    return T;
}

// 删除操作:从二叉排序树中删除指定关键字的节点,并调整树结构以保持其性质
BSTree DeleteBST(BSTree* T, KeyType key) {
    if (*T == NULL) {
        return NULL; // 未找到关键码为key的结点
    }
    else if (key < (*T)->data.key) {
        (*T)->lchild = DeleteBST(&(*T)->lchild, key); // 在左子树中删除
    }
    else if (key > (*T)->data.key) {
        (*T)->rchild = DeleteBST(&(*T)->rchild, key); // 在右子树中删除
    }
    else { // 找到要删除的结点
        BSTNode* p = *T;

        if ((*T)->lchild == NULL) { // 情况1: 只有右子树或没有子树
            *T = (*T)->rchild;
            free(p);
        }
        else if ((*T)->rchild == NULL) { // 情况2: 只有左子树
            *T = (*T)->lchild;
            free(p);
        }
        else { // 情况3: 有两个孩子节点
            // 找到右子树中的最小节点
            BSTNode* minNode = FindMin((*T)->rchild);
            (*T)->data = minNode->data; // 用最小节点的数据替换当前节点的数据
            (*T)->rchild = DeleteBST(&(*T)->rchild, minNode->data.key); // 递归删除最小节点
        }
    }
    return *T;
}

// 中序遍历(用于展示树的内容)
void InOrderTraversal(BSTree T) {
    if (T != NULL) {
        InOrderTraversal(T->lchild);
        printf("Key: %d, Info: %s\n", T->data.key, T->data.otherinfo);
        InOrderTraversal(T->rchild);
    }
}

// 示例main函数用于测试
int main() {
    BSTree T = NULL; // 初始化二叉排序树为空

    // 插入一些节点
    ElemType elements[] = {
        {5, "五"},
        {3, "三"},
        {7, "七"},
        {2, "二"},
        {4, "四"},
        {6, "六"},
        {8, "八"}
    };
    for (int i = 0; i < sizeof(elements) / sizeof(elements[0]); ++i) {
        InsertBST(&T, elements[i]);
    }

    // 展示树的内容
    printf("二叉排序树的中序遍历:\n");
    InOrderTraversal(T);

    // 查找一个节点
    KeyType searchKey = 7;
    BSTree found = SearchBST(T, searchKey);
    if (found != NULL) {
        printf("找到节点,关键字为:%d,信息为:%s\n", found->data.key, found->data.otherinfo);
    }
    else {
        printf("未找到该节点。\n");
    }

    // 删除一个节点
    KeyType deleteKey = 7;
    DeleteBST(&T, deleteKey);
    printf("删除关键字为 %d 的节点后,二叉排序树的中序遍历:\n", deleteKey);
    InOrderTraversal(T);

    return 0;
}
(6)运行结果

2、算法分析

2)平衡二叉树

平衡二叉树(balanced binary tree)

  • 又称AVL树(Adelson-Velskii and Landis);

  • 一棵平衡二又树或者是空树,或者是具有下列性质的二叉排序树

    • ① 左子树与右子树的高度之差的绝对值小于等于1;

    • ② 左子树和右子树也是平衡二叉排序树。

为了方便起见,给每个结点附加一个数字,给出该结点左子树与右子树的高度差。这个数字称为结点的平衡因子(BF)。

平衡因子= 结点左子树的高度-结点右子树的高度

根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是-1、0、或1。

1、失衡二叉排序树的分析与调整



调整原则:

1)降低高度

2)保持二叉树排序树的性质(左子树小于右子树)

2、主要代码

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 定义关键字类型和其他信息类型
typedef int KeyType;
typedef char InfoType[50]; // 假设其他信息为字符串

// 定义数据元素类型
typedef struct {
    KeyType key;        // 关键字项
    InfoType otherinfo; // 其他数据域
} ElemType;

// 定义AVL树节点结构体
typedef struct AVLNode {
    ElemType data;      // 数据域
    int height;         // 节点高度
    struct AVLNode *lchild, *rchild; // 左右孩子指针
} AVLNode, *AVLTree;

// 获取节点的高度
int GetHeight(AVLTree T) {
    if (T == NULL) return -1;
    return T->height;
}

// 更新节点的高度
void UpdateHeight(AVLTree T) {
    int leftHeight = GetHeight(T->lchild);
    int rightHeight = GetHeight(T->rchild);
    T->height = (leftHeight > rightHeight ? leftHeight : rightHeight) + 1;
}

// 右旋
AVLTree RightRotate(AVLTree y) {
    AVLTree x = y->lchild;
    y->lchild = x->rchild;
    x->rchild = y;

    // 更新高度
    UpdateHeight(y);
    UpdateHeight(x);

    return x;
}

// 左旋
AVLTree LeftRotate(AVLTree x) {
    AVLTree y = x->rchild;
    x->rchild = y->lchild;
    y->lchild = x;

    // 更新高度
    UpdateHeight(x);
    UpdateHeight(y);

    return y;
}

// 查找操作:根据给定的关键字查找对应的节点
AVLTree SearchAVL(AVLTree T, KeyType key) {
    if (!T || key == T->data.key)
        return T;
    else if (key < T->data.key)
        return SearchAVL(T->lchild, key);  // 在左子树中继续查找
    else
        return SearchAVL(T->rchild, key);  // 在右子树中继续查找
}

// 插入操作:将新元素插入到合适的位置,保持AVL树的性质
AVLTree InsertAVL(AVLTree *T, ElemType e) {
    if (*T == NULL) { // 如果是空树或到达叶子节点
        *T = (AVLNode *)malloc(sizeof(AVLNode));
        (*T)->data = e;
        (*T)->height = 0;
        (*T)->lchild = (*T)->rchild = NULL;
        return *T;
    }

    if (e.key < (*T)->data.key) {
        InsertAVL(&(*T)->lchild, e);
        if (GetHeight((*T)->lchild) - GetHeight((*T)->rchild) == 2) {
            if (e.key < (*T)->lchild->data.key)
                *T = RightRotate(*T);
            else {
                (*T)->lchild = LeftRotate((*T)->lchild);
                *T = RightRotate(*T);
            }
        }
    } else if (e.key > (*T)->data.key) {
        InsertAVL(&(*T)->rchild, e);
        if (GetHeight((*T)->rchild) - GetHeight((*T)->lchild) == 2) {
            if (e.key > (*T)->rchild->data.key)
                *T = LeftRotate(*T);
            else {
                (*T)->rchild = RightRotate((*T)->rchild);
                *T = LeftRotate(*T);
            }
        }
    }
    // 关键字已存在,不进行插入

    UpdateHeight(*T);
    return *T;
}

// 中序遍历(用于展示树的内容)
void InOrderTraversal(AVLTree T) {
    if (T != NULL) {
        InOrderTraversal(T->lchild);
        printf("Key: %d, Info: %s\n", T->data.key, T->data.otherinfo);
        InOrderTraversal(T->rchild);
    }
}

// 示例main函数用于测试
int main() {
    AVLTree T = NULL; // 初始化AVL树为空

    // 插入一些节点
    ElemType elements[] = {
        {5, "五"},
        {3, "三"},
        {7, "七"},
        {2, "二"},
        {4, "四"},
        {6, "六"},
        {8, "八"}
    };
    for (int i = 0; i < sizeof(elements)/sizeof(elements[0]); ++i) {
        InsertAVL(&T, elements[i]);
    }

    // 展示树的内容
    printf("AVL树的中序遍历:\n");
    InOrderTraversal(T);

    // 查找一个节点
    KeyType searchKey = 7;
    AVLTree found = SearchAVL(T, searchKey);
    if (found != NULL) {
        printf("找到节点,关键字为:%d,信息为:%s\n", found->data.key, found->data.otherinfo);
    } else {
        printf("未找到该节点。\n");
    }

    return 0;
}

3)红黑树 (Red-Black Tree)

定义:

红黑树是一种特殊的二叉搜索树(BST),它通过给每个节点添加一个表示颜色的属性(红色或黑色)来保证树的高度大致平衡,从而确保操作的时间复杂度为O(log n)。

性质:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点总是黑色。
  3. 所有叶子节点(NIL节点,可以认为是空节点)都是黑色。
  4. 如果一个节点是红色,则它的两个子节点必须是黑色。(即从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

应用:

红黑树广泛应用于各种编程语言的标准库中,如C++ STL中的std::mapstd::set,以及Java中的TreeMapTreeSet。它们适用于需要频繁插入、删除和查找操作的场景。


4)B-树 (B-tree)

定义:

B-树是一种自平衡的多路搜索树,可以看作是二叉搜索树的扩展,允许每个节点拥有多个关键字和多个子节点。B-树的设计目的是为了减少磁盘I/O次数,因此非常适合用于文件系统和数据库系统中。

如图一棵 3 阶B-树

性质:

  1. m阶B-树 的每个节点最多有m个孩子。
  2. 除了根节点外,所有节点至少有⌈m/2⌉个孩子(根节点至少有两个孩子,如果它不是叶子)。
  3. 每个节点(除根节点外)至少含有⌈m/2⌉ - 1个关键字,并且至多含有m - 1个关键字。
  4. 所有的叶子节点都在同一层。
  5. 每个节点的关键字按照升序排列,左子树中的所有关键字小于该节点的第一个关键字,右子树中的所有关键字大于该节点的最后一个关键字。

应用:

B-树被广泛应用于数据库索引和文件系统的实现中,因为它能够有效地支持顺序访问和随机访问,同时减少了磁盘I/O次数。


5)B+树 (B+tree)

定义:

B+树是B-树的一种变体,与B-树相比,它对数据存储进行了优化,特别适合于外部存储结构。在B+树中,所有实际的数据都存储在叶子节点中,而内部节点仅用于索引。

性质:

  1. 内部节点只包含关键字信息而不包含指向记录的指针,所有的记录指针都集中在叶子节点中。
  2. 所有的叶子节点形成一个有序链表,便于范围查询。
  3. 叶子节点之间的链接使得范围扫描非常高效。
  4. 满足B-树的所有其他性质。

应用:

B+树常用于数据库管理系统(DBMS)和文件系统中作为索引结构,因为它们能够有效地处理大量数据,并且支持高效的范围查询和顺序扫描。


6)总结

  • 红黑树 主要用于内存中的快速查找、插入和删除操作,适用于需要频繁进行这些操作的应用场景。
  • B-树B+树 更适合用于磁盘等外部存储设备上的数据组织,特别是当数据量巨大时,它们能有效减少磁盘I/O次数,提高读写效率。其中,B+树由于其对范围查询的支持更好,更常用于数据库索引。

四、散列表(哈希表)的查找

1)介绍

散列方法(散列查找法 又叫杂凑法散列法

  • 选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放;
  • 查找时,由同一个函数对给定值k计算地址,将k与地址单元中元素关键码进行比较,确定查找是否成功。

散列函数(杂凑函数):散列方法中使用的转换函数

使用散列表要解决好两个问题

1)构造好的散列函数

  • (a)所选函数尽可能简单,以便提高转换速度;
  • (b)所选函数对关键码计算出的地址,应在散列地址集中致均匀分布,以减少空间浪费。

2)制定一个好的解决冲突的方案

​ 查找时,如果从散列函数计算出的地址中查不到关键码,则应当依据解决冲突的规则,有规律地查询其它相关单元

构造散列表函数考虑的因素

  • 执行速度(即计算散列函数所需时间);
  • 关键字的长度;
  • 散列表的大小;
  • 关键字的分布情况;
  • 查找频率。

2)散列函数的构造方法

1、直接地址法

2、除留余数法

3)处理冲突的方法

1、开放地址法

(1)线性探测法


平均查找长度: ASL = (1 + 2 + 1 + 1+ 1 + 4 + 1 + 2 + 2)/9 = 1.67

(2)二次探测法
(3)伪随机数探测法

依靠伪随机数,哪里有空就填入哪里

2、链地址法(拉链法)

链地址法建立散列表步骤:

Step1:取数据元素的关键字key,计算其散列函数值(地址)。若该地址对应的链表为空,则将该元素插入此链表;否则执行Step2解决冲突。

Step2:根据选择的冲突处理方法,计算关键字key的下一个存储地址。若该地址对应的链表为不为空,则利用链表的前插法或后插法将该元素插入此链表。

链地址法的优点:

非同义词不会冲突,无"聚集"现象链表上结点空间动态申请,更适合子表长不确定的情况。

4)例题:

5)算法分析

6)结论:

散列表技术具有很好的平均性能,优于一些传统的技术

链地址法优于开地址法

除留余数法作散列函数优于其它类型函数

相关推荐
小冯的编程学习之路18 分钟前
【LeetCode】:删除回文子数组【困难】
算法·leetcode·职场和发展
过过过呀Glik35 分钟前
在 Ubuntu 中安装 C++ 版本的 Protocol Buffers
linux·c++·ubuntu·protocol
计算机小混子38 分钟前
C++实现设计模式---单例模式 (Singleton)
开发语言·c++·单例模式
计算机小混子1 小时前
C++实现设计模式---工厂方法模式 (Factory Method)
c++·设计模式·工厂方法模式
IOT-Power1 小时前
<C++学习>C++ std 多线程教程
c++
廖显东-ShirDon 讲编程1 小时前
《零基础Go语言算法实战》【题目 1-16】字符串的遍历与比较
算法·程序员·go语言·web编程·go web
还是车万大佬1 小时前
C语言二级考试
c语言·开发语言·笔记
卡戎-caryon1 小时前
【应用篇】09.实现简易的Shell命令行解释器
c++·笔记·shell·命令行解释器
极客小张2 小时前
基于STM32的智能电表可视化设计:ESP8266、AT指令集、python后端Flask(代码示例)
c语言·stm32·单片机·嵌入式硬件·物联网·tcp/ip·毕业设计
小庞在加油2 小时前
【C++开源库】tinyxml2解析库使用介绍
c++·开源·tinyxml2解析库