一、查找的基本概念
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)。
性质:
- 每个节点要么是红色,要么是黑色。
- 根节点总是黑色。
- 所有叶子节点(NIL节点,可以认为是空节点)都是黑色。
- 如果一个节点是红色,则它的两个子节点必须是黑色。(即从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
应用:
红黑树广泛应用于各种编程语言的标准库中,如C++ STL中的std::map
和std::set
,以及Java中的TreeMap
和TreeSet
。它们适用于需要频繁插入、删除和查找操作的场景。
4)B-树 (B-tree)
定义:
B-树是一种自平衡的多路搜索树,可以看作是二叉搜索树的扩展,允许每个节点拥有多个关键字和多个子节点。B-树的设计目的是为了减少磁盘I/O次数,因此非常适合用于文件系统和数据库系统中。
如图一棵 3 阶B-树
性质:
- m阶B-树 的每个节点最多有m个孩子。
- 除了根节点外,所有节点至少有⌈m/2⌉个孩子(根节点至少有两个孩子,如果它不是叶子)。
- 每个节点(除根节点外)至少含有⌈m/2⌉ - 1个关键字,并且至多含有m - 1个关键字。
- 所有的叶子节点都在同一层。
- 每个节点的关键字按照升序排列,左子树中的所有关键字小于该节点的第一个关键字,右子树中的所有关键字大于该节点的最后一个关键字。
应用:
B-树被广泛应用于数据库索引和文件系统的实现中,因为它能够有效地支持顺序访问和随机访问,同时减少了磁盘I/O次数。
5)B+树 (B+tree)
定义:
B+树是B-树的一种变体,与B-树相比,它对数据存储进行了优化,特别适合于外部存储结构。在B+树中,所有实际的数据都存储在叶子节点中,而内部节点仅用于索引。
性质:
- 内部节点只包含关键字信息而不包含指向记录的指针,所有的记录指针都集中在叶子节点中。
- 所有的叶子节点形成一个有序链表,便于范围查询。
- 叶子节点之间的链接使得范围扫描非常高效。
- 满足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)结论:
散列表技术具有很好的平均性能,优于一些传统的技术
链地址法优于开地址法
除留余数法作散列函数优于其它类型函数