- 集合操作的实现
1.1 集合的并运算
1.2 集合的交运算 - 顺序查找
2.1 顺序存储结构
2.2 基于顺序存储结构的顺序查找算法
2.3 基于有序顺序表的顺序查找算法
2.4 索引顺序表 - 并查集
3.1 并查集类的描述
3.2 查找算法
3.3 合并操作 - 二叉排序树 (BST)
4.1 二叉排序树结点定义
4.2 二叉排序树类定义
4.3 查找算法
4.4 插入算法
4.5 建立算法
4.6 删除算法 - 二叉平衡树 (AVL)
5.1 四种失衡情况
5.2 插入和调整实例 - 2-3 树
6.1 两种内部结点的含义
6.2 插入操作
6.3 删除操作 - B 树
7.1 查找
7.2 插入
7.3 删除 - 哈希表
8.1 哈希函数的构造方法
8.2 处理冲突方法
# 一、集合操作的实现
# 1. 集合的并运算
// 集合的并运算 | |
void Union(SeqList<int> &LA,SeqList<int> &LB) | |
{ | |
int n1=LA.Length(),n2=LB.Length(); | |
int i,k,x; | |
for(i=0;i<n2;++i) | |
{ | |
x=LB.getData(i);// 在 LB 中取一元素 | |
k=LA.Search(x);// 在 LA 中搜索它 | |
if(k==0)// 若在 LA 中未找到,则插入它 | |
{ | |
LA.Insert(n1,x);// 插入到第 n1 个表项位置 | |
n1++; | |
} | |
} | |
} |
# 2. 集合的交运算
// 集合的交运算 | |
void Intersection(SeqList<int> &LA,SeqList<int> &LB) | |
{ | |
int n1=LA.Length(); | |
int x,k,i=0; | |
while(i<n1) | |
{ | |
x=LA.getData(i);// 在 LA 上取一元素 | |
k=LB.Search(x);// 在 LB 上搜索它 | |
if(k==0) | |
{ | |
LA.Remove(i,x); | |
n1--;// 在 LA 中删除它 | |
} | |
else | |
{ | |
i++; | |
} | |
} | |
} |
# 二、顺序查找
# 1. 顺序存储结构
// 顺序存储结构类型定义如下 | |
typedef int Key; // 定义 Key 类型为 int | |
struct Rec | |
{ | |
Key key; | |
...; | |
}; | |
Rec S[n+1];// 元素分别存放在 S [1] 至 S [n] |
# 2. 基于顺序存储结构的顺序查找算法
# 普通实现算法

int Search(Rec S[],Key x,int n)// 基于顺序存储结构的顺序查找算法 | |
{ | |
int i=1;// 从头开始查看 | |
while(S[i].key!=x && i<=n) | |
{ | |
i++;// 往后查找 | |
} | |
if(i<=n) | |
{ | |
return i; | |
} | |
else | |
{ | |
return 0; | |
} | |
} |
# 改进算法 1

改进算法 1 的核心改进是引入 “监视哨”,
通过在数组尾端放置要查找的元素,
消除了循环中的边界判断,从而简化逻辑并提高查找效率。
// 改进算法 1 | |
int Search(Rec S[],Key x,int n)// 基于顺序存储结构的顺序查找算法 | |
{ | |
// 顺序查找关键码为 x 的数据对象 | |
// 使用第 n+1 号位置作为控制搜索自动结束的 “监视哨” 使用 | |
S[n+1].key=x;// 将 x 送 n 号位置设置监视哨 | |
int i=1; | |
while(S[i].key!=x) | |
{ | |
i++;// 从前向后顺序搜索 | |
} | |
if(i==n+1) | |
{ | |
return 0; | |
} | |
else | |
{ | |
return i; | |
} | |
} |
# 改进算法 2

“改进算法 2” 是在 “改进算法 1” 的基础上进一步优化:
将哨兵移到 S [0],不再需要额外空间;
从后向前搜索,在某些使用场景更高效;
依旧保持单条件循环,逻辑简洁无越界。
// 改进算法 2 | |
int Search(Rec S[],Key x,int n)// 基于顺序存储结构的顺序查找算法 | |
{ | |
// 顺序查找关键码为 x 的数据对象 | |
// 使用第 0 号位置作为控制搜索自动结束的 “监视哨” 使用 | |
S[0].key=x;// 将 x 送 0 号位置设置监视哨 | |
int i=n; | |
while(S[i].key!=x) | |
{ | |
i--;// 从后向前顺序搜索 | |
} | |
if(i==0) | |
{ | |
return 0; | |
} | |
else | |
{ | |
return i; | |
} | |
} |
# 3. 基于有序顺序表的顺序查找算法
# 普通实现算法

int Search(Rec S[],Key x,int n)// 有序顺序表的顺序查找算法 | |
{ | |
// 顺序搜索关键码为 x 的数据对象 | |
for(int i=1;i<=n;++i) | |
{ | |
if(S[i].key==x) | |
{ | |
return i;// 成功 | |
} | |
else | |
{ | |
if(S[i].key>x) | |
{ | |
break; | |
} | |
} | |
} | |
return 0;// 顺序搜索失败,返回失败信息 | |
} |
# 二分查找算法 (非递归算法)
int Search_Bin(Rec S[],Key x,int n)// 有序顺序表的顺序查找算法 | |
{ | |
// 二分查找的非递归算法 S [] 为有序表 | |
int low=1; | |
int high=n; | |
int mid; | |
while(low<=high) | |
{ | |
mid=(low+high)/2; | |
if(x==S[mid].key) | |
{ | |
return mid; | |
} | |
else if(x<S[mid].key) | |
{ | |
high=mid-1; | |
} | |
else | |
{ | |
low=mid+1; | |
} | |
} | |
return 0; | |
} |
# 二分查找算法 (递归算法)
int Search_Bin(Rec S[],Key x,int low,int high)// 有序顺序表的顺序查找算法 | |
{ | |
// 在主函数 main 中调用该函数时实参为 Search_Bin (S,x,1,n) | |
if(low>high) | |
{ | |
return 0;// 查找不成功 | |
} | |
else | |
{ | |
int mid=(low+high)/2; | |
if(x==S[mid].key) | |
{ | |
return mid; | |
} | |
else if(x<S[mid].key) | |
{ | |
return Search_Bin(S,x,low,mid-1); | |
} | |
else | |
{ | |
return Search_Bin(S,x,mid+1,high); | |
} | |
} | |
} |

# 4. 索引顺序表


// 索引表的结构 | |
struct indexItem | |
{ | |
Key maxKey;// 当前块中最大关键字 | |
int stadr;// 当前块在顺序表中的起始地址(索引号) | |
}IndexTable[MaxSize];// 索引表 | |
struct Rec | |
{ | |
Key key; | |
... | |
}Table[n];// 顺序表 |
# 三、并查集






# 1. 并查集类的描述
// 并查集类的描述 | |
#define MaxSize 100 | |
int parent[MaxSize];// 并查集的父结点数组 | |
int n;// 集合元素个数 | |
// 初始化操作 | |
for(int i=0;i<n;i++) | |
{ | |
parent[i]=-1; | |
} |
# 2. 查找算法
// 查找算法 | |
// 实现 find (i) 只需简单地沿着结点 i 的 parent 向上移动,直至到达 parent 值为 - 1 的结点 | |
int SimpleFind(int i)// 找到含元素 i 的树的根 | |
{ | |
while(parent[i]>=0) | |
{ | |
i=parent[i]; | |
} | |
return i; | |
} |
# 3. 合并操作

// 合并操作 | |
// 这里假设采用令第一棵树为第二棵树的子树的策略 | |
// 用以 i 为根和以 j (i!=j) 为根的两个不相交集合的 并 取代它们 | |
void SimpleUnion(int i,int j) | |
{ | |
parent[i]=j; | |
} |
# 四、二叉排序树 (BST)
(二叉搜索树 / 二叉检索树 / 二叉查找树)


# 1. 二叉排序树结点定义
// 二叉排序树也是二叉树,因此,通常取二叉链表作为二叉排序树的存储结构 | |
template<class T> | |
struct BTreeNode// 结点结构 | |
{ | |
T data; | |
BTreeNode *lchild,*rchild;// 左右孩子结点 | |
}; |
# 2. 二叉排序树类定义
template<class T> | |
class BSTree | |
{ | |
BTreeNode<T> *root; | |
BTreeNode<T> *searchBST(T x,BTreeNode<T> *p);// 递归查找函数 | |
int insert(T x,BTreeNode<T> *&bst);// 递归插入函数 | |
int remove(T x,BTreeNode<T> *&p);// 删除函数 | |
public: | |
BSTree(BTreeNode<T> *p=0)// 构造函数 | |
{ | |
root=p; | |
} | |
BTreeNode<T> *searchBST(T x);// 查找函数,调用递归查找,供 main 函数调用 | |
BTreeNode<T> *searchBST1(T x);// 非递归查找函数 | |
T min();// 获取最小值 | |
T max();// 获取最大值 | |
int insert(T x);// 插入函数,其调用递归插入,供 main 函数调用 | |
int insert1(T x);// 非递归插入函数 | |
void CreateBST();// 建立二叉排序树 | |
int remove(T x);// 删除函数 | |
}; |
# 3. 查找算法

# 递归算法
// 递归算法 | |
//x:要查找的关键字 | |
//p:当前要搜索的子树的根结点指针 | |
template<class T> | |
BTreeNode<T> *BSTree<T>::searchBST(T x,BTreeNode<T> *p) | |
{ | |
//p==nullptr(树空),说明没找到;或者 x==p->data,说明当前结点就是要找的。 | |
if(!p || (x==p->data)) | |
{ | |
return p; | |
} | |
else if(x<p->data) | |
{ | |
return searchBST(x,p->lchild);// 在左子树中继续查找 | |
} | |
else | |
{ | |
return searchBST(x,p->rchild);// 在右子树中继续查找 | |
} | |
} | |
//public 的 searchBST 是外部调用接口 | |
// 递归查找函数对应的公有函数 | |
template<class T> | |
BTreeNode<T> *BSTree<T>::searchBST(T x)// 查找函数,调用递归查找,供 main 函数调用 | |
{ | |
return searchBST(x,root); | |
} |
# 非递归算法
// 非递归算法 | |
template<class T> | |
BTreeNode<T> *BSTree<T>::searchBST1(T x) | |
{ | |
BTreeNode<T> *TT=root; | |
while(TT!=0)// 当前结点不为空,就继续查 | |
{ | |
if(x==TT->data) | |
{ | |
return TT; | |
} | |
else if(x<TT->data) | |
{ | |
TT=TT->lchild;// 在左子树中找 | |
} | |
else | |
{ | |
TT=TT->rchild;// 在右子树中找 | |
} | |
} | |
return 0; | |
} |



# 3. 插入算法



# 递归算法
// 递归算法 | |
//*&bst 是指向结点指针的引用 | |
template<class T> | |
int BSTree<T>::insert(T x,BTreeNode<T> *&bst)// 私有函数 | |
{ | |
BTreeNode<T> *p; | |
if(bst==0) | |
{ | |
p=new BTreeNode<T>; | |
p->data=x; | |
p->lchild=p->rchild=NULL; | |
bst=p; | |
return 1;// 插入成功 | |
} | |
if(x<bst->data) | |
{ | |
return insert(x,bst->lchild);// 在左子树中插入 | |
} | |
else if(x>bst->data) | |
{ | |
return insert(x,bst->rchild);// 在右子树中插入 | |
} | |
else | |
{ | |
return -1;// 重复值,插入不成功 | |
} | |
} | |
// 调用插入递归函数 插入操作公有函数 | |
template<class T> | |
int BSTree<T>::insert(T x) | |
{ | |
int ret;//return | |
ret=insert(x,root); | |
if(ret==-1) | |
{ | |
cout <<"HAVE FOUND"; | |
} | |
else | |
{ | |
cout <<"insert ok"; | |
} | |
return ret; | |
} |
# 非递归算法
// 非递归算法 | |
//TT:当前遍历的结点指针,从根结点开始往下找 | |
//f:指向 TT 的父结点,用于在插入时确定新结点应插在左还是右 | |
//p:新建结点指针,在后面创建新结点时使用 | |
template<class T> | |
int BSTree<T>::insert1(T x) | |
{ | |
BTreeNode<T> *TT,*f,*p; | |
TT=root; | |
f=0; | |
while(TT!=0) | |
{ | |
if(x==TT->data)// 重复值,插入不成功 | |
{ | |
return -1; | |
} | |
else if(x<TT->data) | |
{ | |
f=TT; | |
TT=TT->lchild;// 在左子树中找 | |
} | |
else | |
{ | |
f=TT; | |
TT=TT->rchild;// 在右子树中找 | |
} | |
} | |
// 当循环结束时,TT == 0,说明找到了插入位置 | |
p=new BTreeNode<T>; | |
p->data=x;// 新结点生成 | |
p->lchild=p->rchild=0; | |
if(f==0)// 树原来是空的,新结点就是根 | |
{ | |
root=p;// 根结点 | |
} | |
else if(x>f->data) | |
{ | |
f->rchild=p;// 作为右子树 | |
} | |
else | |
{ | |
f->lchild=p;// 作为左子树 | |
} | |
return 1;// 插入成功 | |
} |
# 4. 建立算法

// 建立算法 | |
template<class T> | |
void BSTree<T>::CreateBST() | |
{ | |
root=0; | |
T x; | |
cin >>x; | |
while(x!=-1) | |
{ | |
insert(x); | |
cin >>x; | |
} | |
} |
# 5. 删除算法





找 “前驱结点”
1. 前驱:p 左子树中 最大 的结点(即往左走一次,再一直往右走)
2. 用前驱的值替换 p->data
3. 再从左子树里删掉那个前驱结点
// 递归删除算法 | |
template<class T> | |
int BSTree<T>::remove(T x,BTreeNode<T> *&p) | |
{ | |
if(!p)// 树空或未找到 | |
{ | |
return 0; | |
} | |
if(x<p->data)// 去左子树删 | |
{ | |
return remove(x,p->lchild); | |
} | |
else if(x>p->data)// 去右子树删 | |
{ | |
return remove(x,p->rchild); | |
} | |
else// 找到要删除的结点 p | |
{ | |
BTreeNode<T> *q=p; | |
//1. 没有左右子树(叶子结点) | |
if(!p->lchild && !p->rchild) | |
{ | |
delete p; | |
p=nullptr; | |
} | |
//2. 只有左子树 | |
else if(p->lchild && !p->rchild) | |
{ | |
p=p->lchild; | |
delete q; | |
} | |
//3. 只有右子树 | |
else if(!p->lchild && p->rchild) | |
{ | |
p=p->rchild; | |
delete q; | |
} | |
//4. 有左右子树 | |
else | |
{ | |
// 找左子树中最大的结点 (前驱) | |
BTreeNode<T> *s=p->lchild; | |
while(s->rchild) | |
{ | |
s=s->rchild;// 找前驱 | |
} | |
p->data=s->data;// 替换,注意建议复制数据 | |
// 因为 s 是前驱结点,所以是一个叶子结点,或者只有一个左孩子 | |
remove(p->data,p->lchild);// 删除前驱 | |
} | |
return 1; | |
} | |
} |
# 五、二叉平衡树 (AVL)


在 AVL 树(一种自平衡二叉查找树)中,
平衡因子(Balance Factor,简称 BF) 是用于衡量结点 “平衡程度” 的一个值。
🌳 定义:
对于任意一个结点 node :
📘 取值范围:
为了让整棵树保持 “平衡”,AVL 树要求:
也就是说:
- BF = 0:左右子树高度相等,完全平衡
- BF = +1:左子树比右子树高 1,稍微左重
- BF = -1:右子树比左子树高 1,稍微右重
- |BF| > 1:该结点失衡,需要通过旋转(LL、RR、LR、RL)调整平衡


# 1. 四种失衡情况
LL 型:右旋失衡结点
RR 型:左旋失衡结点
LR 型:左旋失衡结点左孩子,右旋失衡结点
RL 型:右旋失衡结点右孩子,左旋失衡结点
# 2. 插入和调整实例



# 六、2-3 树

# 1. 两种内部结点的含义
(1)2 - 结点(Two-Node)
- 含有 一个关键字(比如
dataL.key) - 有 两个子树(左子树
LeftChild、右子树MiddleChild)
规则:
LeftChild 的所有 key < dataL.key | |
MiddleChild 的所有 key > dataL.key |
也就是说,它就像普通二叉搜索树:
2-结点 | |
[dataL] | |
/ \ | |
LeftChild MiddleChild |
(2)3 - 结点(Three-Node)
- 含有 两个关键字:
dataL.key和dataR.key - 有 三个子树:
LeftChild、MiddleChild、RightChild
并且:
dataL.key < dataR.key |
规则:
LeftChild 中所有关键字 < dataL.key | |
MiddleChild 中所有关键字 > dataL.key 且 < dataR.key | |
RightChild 中所有关键字 > dataR.key |
结构示意:
3-结点 | |
[ dataL | dataR ] | |
/ | \ | |
LeftChild MiddleChild RightChild |
# 2. 插入操作

手工方法
特点:
2-3 树的插入操作和二叉树既有相似的地方又有不同的地方。
相似之处在于两者都要先通过搜索,找到插入的位点;
不同之处在于这个插入的位点总是位于叶子结点,而非一个任意结点;另外,2-3 树的插入操作是将插入值 value 直接放进待插入的结点,而不是新建一个结点。
但是,2-3 树存在两种类型的结点(2 - 结点和 3 - 结点),因此我们要分为两种情况讨论:
# 2.1 插入到 2 - 结点
将一个值 value 插入到 2 - 结点的情况很简单,只需要将 value 放入正确的位置使之变为 3 - 结点即可。
例如将 4 插入到下面的树中,将会得到:

# 2.2 插入到 3 - 结点
插入到 3 - 结点的情况会比较麻烦,因为结点已经满了,在这种情况下,2-3 树采用的策略是分裂成两个结点。
分裂:
分裂的过程可以概括为:原结点保留三个值(新来的值以及结点中原有的两个值)中的最小值,新分裂出的结点用来存放最大值,而将中间值向上传递到父结点中。
换句话说就是将中间值插入到父结点中,此时,还需要讨论父结点是 2 - 结点还是 3 - 结点,所以分情况讨论:
- 情况 1:父结点存在,且为 2 - 结点。
这种情况下只需将中间值插入到父结点即可。
下面中的例子中,如果要在向 2-3 树中插入 1,插入后原结点分裂成两个结点,并将 2 传递到父结点,最后父结点变为 3 - 结点。

- 情况 2:父结点存在,且为 3 - 结点。
这时父结点也需要分裂,因此分裂后父结点再将其中间值向上传递,整个过程递归地进行直到遇到根结点。例如,我们还是在绿色的 3 - 结点中插入 9,插入后原结点分裂成两个结点,并将 8 传递到父结点中,这时发现父结点也为 3 - 结点,所以父结点的父结点也要分裂,分裂后我们把中间值 6 传递到上一层

# 3. 删除操作

当待删除的值在叶子结点上时,可以分为下面两大类:
# 3.1 叶子结点为 3 - 结点
这种情况最简单,直接删除即可,3 - 结点变为 2 - 结点

# 3.2 叶子结点为 2 - 结点
当叶子结点为 2 - 结点时,需要分别查看兄弟结点和父结点的类型,因此,再将它们分为三种情况:
# 3.2.1 兄弟结点为 3 - 结点
兄弟结点为 3 - 结点。这种情况下,我们需要向兄弟结点借一个最近的值,来补充删除后空的叶子结点。
下面的例子中,如果删除的值为 6,我们需要向兄弟结点借一个值 8,但借来后发现不满足大小关系(7 > 8),所以我们需要再交换一下父结点和叶子结点的值。

# 3.2.2 兄弟结点为 2 - 结点,父结点为 3 - 结点
兄弟结点为 2 - 结点,父结点为 3 - 结点。因为兄弟结点为 2 - 结点,所以就不能向兄弟结点借一个值了,我们转而向父结点借,并与兄弟结点结合,最后父结点从 3 - 结点变成了 2 - 结点。

# 3.2.3 父结点和兄弟结点都为 2 - 结点
父结点和兄弟结点都为 2 - 结点。这种情况是最麻烦的,首先我们要让父结点和兄弟结点合并。例如下方,如要删除 1,则 2 和 3 进行合并。

我们发现原来的父结点内值为空,为了填补这个空结点,我们要再对父结点执行一次删除操作。
注意这里我们仅仅只是调整树的结构,而非删除任何值。
接下来我们查看这个结点的兄弟结点和父结点,如果它的兄弟结点是 3 - 结点,我们就向兄弟结点借一个值来填补这个空值,最后调整结点之间的值使之满足大小关系就好了。

如果父结点是 3 - 结点,那么我们就向其父结点中借一个值来补充这个空值。

假如父结点和兄弟结点都不是 3 - 结点,我们就再次合并父结点和兄弟结点,并把空结点传递到上一层,然后再看前面的情况是否满足(在上一层重新检测这三个情况中属于哪一种),这就变成了一个递归的过程,当这个空结点是这棵 2-3 树的根结点时,我们就删除这个结点,并且它的孩子结点为树的根结点,此时树的高度减 1。

时间复杂度分析

# 七、B 树


这里把外部结点作为叶子结点

注意:根结点最少 2 个分支,1 个元素;除根结点外,最少 个分支, 个元素
# 1. 查找

# 2. 插入

上溢出:中间元素 上移,两边分裂

# 3. 删除

1. 删除 45:45 是非叶子结点,这里利用直接后继替换删除结点

2. 删除 68:删除 68 后,出现下溢出现象,因此要向兄弟结点借一个元素;首先判断兄弟结点是否够借(下面例子中可向左兄弟或右兄弟借一个元素),以向右兄弟借为例,向右兄弟借一个元素,同时为了保证 B 树的有序性质,因此需要同时调整其父结点

3. 删除 86:删除 86 后,出现下溢出现象,并且兄弟结点不满足能借出的条件;因此选择与兄弟结点进行合并(与左兄弟结点或右兄弟结点均可),这里以和右兄弟结点合并为例,此时又要考虑到父结点,因此合并操作为:父结点下移到左,然后进行右并(和左兄弟结点合并的操作也是父结点下移到左,进行右并)

4. 删除 30:用直接后继替换 30 后,41 出现下溢出,且左右兄弟结点均不够借,因此父结点下移到左,然后右并;之后 40 出现下溢出,兄弟结点够借,因此父结点下移,兄弟结点元素 51 上移;最后调整子树分支

5. 删除 57

6. 删除 53:删除 53 后,60 下溢出,且兄弟结点不可借,则与兄弟结点进行合并;之后 76 出现下溢出,且兄弟结点不可借,则与兄弟结点进行合并

# 八、哈希表

# 1. 哈希函数的构造方法
# 1.1 直接定址法

# 1.2 除留余数法

# 1.3 数字分析法

# 1.4 折叠法

# 1.5 平方取中法

# 1.6 随机数法

# 2. 处理冲突方法

# 2.1 开放定址法

# 2.2 链地址法

# 存储结构
// 哈希表最大容量 | |
#define MaxSize 100 | |
// 链表节点结构(用于链地址法存储冲突元素) | |
struct LinkNode | |
{ | |
int data; // 存储关键字 | |
LinkNode *link; // 指向下一个冲突节点的指针 | |
}; |
# 插入算法
// 哈希表插入算法(头插法) | |
bool InsertHash(int key) | |
{ | |
// 1. 计算哈希地址 | |
int y = H(key); | |
// 2. 创建新节点 | |
LinkNode *p = new LinkNode; | |
p->data = key; | |
p->link = nullptr; | |
// 3. 头插法插入到对应哈希桶的链表头部 | |
p->link = HT[y]; // 新节点指向原链表头 | |
HT[y] = p; // 更新链表头为新节点 | |
return true; // 插入成功 | |
} |
# 查找算法

// 哈希表查找算法 | |
int SearchHash(int key) | |
{ | |
// 1. 计算哈希地址 | |
int y = H(key); | |
// 2. 遍历对应哈希桶的链表 | |
LinkNode *p = HT[y]; | |
while (p != nullptr) | |
{ | |
if (p->data == key) | |
{ | |
return 1; // 查找成功,返回 1 | |
} | |
p = p->link; // 遍历下一个节点 | |
} | |
return -1; // 查找失败,返回 - 1 | |
} |