关键码:可以标识一个记录(数据元素、结点、顶点)的某个数据项
键值:关键码的值
主关键码:可以唯一标识一个记录的关键码
次关键码:不能唯一标识一个记录的关键码
查找的方法取决于查找表的结构。
如何评价查找算法的效率呢?
和关键码的比较次数
关键码的比较次数与哪些因素有关呢?
平均查找长度(Average Search Length):查找算法进行的关键码比较次数的数学期望值
对顺序表而言
ASL = nP1+(n-1)P2+…+2Pn-1-+Pn
若查找概率无法事先测定,则查找过程采取的改进办法是,附设一个访问频度域或者在每次查找之后,将刚刚查找到的记录直接移至表尾的位置上。
int SeqSearch1(int r[ ], int n, int k)
{
int i = n;
while (i > 0 && r[i] != k)
i--;
return i;
}
//改进前
int LineSearch :: SeqSearch1(int k)
{
int i = n;
while (i > 0 && data[i] != k)
i--;
return i;
}
//改进后
int LineSearch :: SeqSearch2(int k)
{
int i = n;
data[0] = k;
while (data[i] != k)
i--;
return i;
}
int LineSearch :: BinSearch1(int k) /*查找集合存储在r[1]~r[n]*/
{
int mid, low = 1, high = n; /*初始查找区间是[1, n]*/
while (low <= high) /*当区间存在时*/
{
mid = (low + high) / 2;
if (k < data[mid]) high = mid - 1;
else if (k > data[mid]) low = mid + 1;
else return mid; /*查找成功,返回元素序号*/
}
return 0; /*查找失败,返回0*/
}
int LineSearch :: BinSearch2(int low, int high, int k)
{
int mid;
if (low > high) return 0; /*递归的边界条件*/
else {
mid = (low + high) / 2;
if (k < data[mid]) return BinSearch2(low, mid-1, k);
else if (k > data[mid]) return BinSearch2(mid+1, high, k);
else return mid; /*查找成功,返回序号*/
}
}
二叉链表
class BiSortTree
{
public:
BiSortTree(int a[ ], int n);
~ BiSortTree( ) {Release(root);}
BiNode<int> *InsertBST(int x) {return InsertBST(root, x);}
void DeleteBST(BiNode<int> *p, BiNode<int> *f );
BiNode<int> *SearchBST(int k) {return SearchBST(root, k);}
private:
BiNode<int> *InsertBST(BiNode<int> *bt , int x);
BiNode<int> *SearchBST(BiNode<int> *bt, int k);
void Release(BiNode<DataType> *bt);
BiNode<int> *root;
};
在二叉排序树中查找给定值 k 的过程是
(1)若 bt 是空树,则查找失败;
(2)若k=bt->data,则查找成功;
(3)若k<bt->data,则在 bt 的左子树上查找;
(4)若k > bt->data,在 bt 的右子树上查找。
二叉排序树的查找效率在于只需查找二个子树之一
BiNode<int> * BiSortTree :: SearchBST(BiNode<int> *bt, int k)
{
if (bt == nullptr) return nullptr;
if (bt->data == k) return bt;
else if (bt->data > k) return SearchBST(bt->lchild, k);
else return SearchBST(bt->rchild, k);
}
BiNode<int> * BiSortTree::InsertBST(BiNode<int> *bt, int x)
{
if (bt == nullptr) {
BiNode<int> *s = new BiNode<int>; s->data = x;
s->lchild = s->rchild = nullptr;
bt = s;
return bt;
}
else if (bt->data > x) bt->lchild = InsertBST(bt->lchild, x);
else bt->rchild = InsertBST(bt->rchild, x);
}
BiSortTree::BiSortTree(int a[ ], int n)
{
root = nullptr;
for (int i = 0; i < n; i++)
root = InsertBST(root, a[i]);
}
void CreateBST(BSTree *bst){
KeyType key;
*bst=NULL;
scanf ("%d",&key);
while(key!=ENDKEY)
{
InsertBST(bst,Key);
scanf("%d",&key)
}
}
f->lchild = nullptr;
f->lchild = p->rchild;
BiNode *par = p, *s = p->lchild;
while (s->rchild != nullptr)
{
par = s;
s = s->rchild;
}
p->data = s->data;
par->rchild = s->lchild;
特殊情况:左子树中的最大值结点是被删结点的孩子
if (p == par) par->lchild = s->lchild;
template <typename DataTypa>
void BiSortTree::DeleteBST(BiNode<int> *p, BiNode<int> *f )
{
if ((p->lchild == nullptr) && (p->rchild == nullptr)) { //p为叶子
f->lchild = NULL; delete p; return;
}
if (p->rchild == nullptr) { //p只有左子树
f->lchild = p->lchild; delete p; return;
}
if (p->lchild == nullptr) { //p只有右子树
f->lchild = p->rchild; delete p; return;
}
BiNode<int> *par = p, *s = p->lchild; /*p的左右子树均不空*/
while (s->rchild != nullptr) /*查找左子树的最右下结点*/
{
par = s;
s = s->rchild;
}
p->data = s->data;
if (par == p) par->rchild = s->lchild;
else par->lchild = s->lchild;
delete s;
/*查找右子树的最左下结点*/
BiNode<int> *par = p, *s = p->rchild;
while (s->lchild != nullptr)
{
par = s;
s = s->lchild;
}
p->data = s->data;
if (par == p) par->lchild = s->rchild;
else par->rchild = s->rchild;
delete s;
二叉排序树的深度取决于给定查找集合的排列,即结点的插入顺序
AVL(由 Adelson-Velsky 和 Landis 共同发明)
算法:平衡调整
输入:平衡二叉树,新插入结点A
输出:新的平衡二叉树
1. 找到最小不平衡子树的根结点 D
2. 根据结点A和结点D之间的关系,判断调整类型
3. 根据类型、遵循**扁担原理**和**旋转优先**原则进行相应调整
(1)**LL型、RR型:调整一次**
(2)**LR型、RL型:调整两次**
LL 顺时针
RR 逆时针
LR 先逆后顺
RL先顺后逆
例 1:设序列{40, 35, 20},构造平衡二叉树
新插入结点20和最小不平衡子树根结点40之间的关系——LL型,顺时针旋转
扁担原理:将根结点看成是扁担中肩膀的位置
例 2:设序列{40, 35, 20,15, 25, 10},构造平衡二叉树
新插入结点10和最小不平衡子树根结点35之间的关系——LL型
扁担原理:将根结点看成是扁担中肩膀的位置
旋转优先:旋转下来的结点作为新根结点的孩子
新插入结点40和最小不平衡子树根结点20之间的关系——RR型,逆时针
扁担原理:将根结点看成是扁担中肩膀的位置
新插入结点25和最小不平衡子树根结点35之间的关系——LR型,先逆后顺
B树:一棵m阶的B树 或者为空树,或者为满足下列特性的m叉树:
(1)每个结点至多有 m 棵子树;
(2)根结点至少有两棵子树;
(3)除根结点和叶子结点外,所有结点至少有⌈ m/2 ⌉棵子树;
(4)所有结点都包含以下数据:
(n,A0,K1,A1,K2,…,Kn,An)
其中,n(⌈m/2⌉ - 1≤n≤m -1)为关键码的个数,Ki(1≤i≤n)为关键码,且Ki < Ki +1(1≤i≤n-1),Ai(0≤i≤n)为指向子树根结点的指针,且指针Ai 所指子树中所有结点的关键码均小于K i+1 大于Ki 。
(5)叶子结点都在同一层;
假定在 m 阶 B 树中插入关键码key,设n=m-1,插入过程如下:
(1)定位:确定关键码key应该插入哪个终端结点并返回该结点的指针p。
若 p 中的关键码个数小于n,则直接插入关键码key;
否则,结点 p 的关键码个数溢出,执行“分裂——提升”过程。
(2)分裂——提升:将结点 p“分裂”成两个结点,分别是p1和p2,把中间的关键码k“提升”到父结点,并且k的左指针指向p1,右指针指向p2。
如果父结点的关键码个数也溢出,则继续执行“分裂——提升”过程。显然,这种分裂可能一直上传,如果根结点也分裂了,则树的高度增加了一层。
B树的删除
散列的基本思想:在记录的关键码和存储地址之间建立一个确定的对应关系,通过计算得到待查记录的地址。
哈希函数是一个映象,即:将关键字的集合映射到某个地址集合上。
散列表:采用散列技术存储查找集合的连续存储空间
散列函数:将关键码映射为散例表中适当存储位置的函数。
散列地址:由散列函数所得的存储地址。
根据设定的哈希函数H(key)和所选中的处理冲突的方法,将一组关键字映象到一个有限的、地址连续的地址集(区间)上,并以关键字在地址集中的“象”作为相应记录在表中的存储位置,如此构造所得的查找表称之为“哈希表”。
在一般情况下,需在关键字与记录在表中的存储位置之间建立一个函数关系,以f(key)作为关键字为key的记录在表中的位置,通常称这个函数f(key)为哈希函数。
很难找到一个不产生冲突的哈希函数。一般情况下,只能选择恰当的哈希函数,使冲突尽可能少地产生。
如何设计散列函数?
(1)计算简单。散列函数不应该有很大的计算量,否则会降低查找效率。
(2)地址均匀。函数值要尽量均匀散布在地址空间,保证存储空间的有效利用并减少冲突。
若是非数字关键字,则需先对其进行数字化处理。
直接定址法
散列函数是关键码的线性函数,即:
H(key) = a * key + b (a,b为常数)
例 1:关键码集合为{10, 30, 50, 70, 80, 90},选取的散列函数为H(key)=key/10,散列表构造过程如下:
适用于:事先知道关键码,关键码集合不是很大且连续性较好
关键字第一个字母/2再对应到此地址的内容。
数字分析法
假设关键字集合中的每个关键字都是由S位数字组成(u1,u2,…,us),分析关键字集中的全体,并从中提取分布均匀的若干位或它们的组合作为地址。
平方取中法
对关键码平方后,按散列表大小,取中间的若干位作为散列地址。
例 3:散列地址为 2 位,设计平方取中法的散列函数。
不同关键字会以较高的概率产生不同的哈希地址
适用于:事先不知道关键码的分布且关键码的位数不是很大
折叠法
◆将关键字分割成若干部分,然后取它们的叠加和为哈希地址。
例:当图书馆馆藏不到1000时,有图书编号为12360324711202065,那它对应的哈希地址应该为多少?
除留余数法
H(key)=key MOD p
表长是m,p是<=m的最大素数
p小于等于表长(最好接近表长)的最大素数或不包含小于20质因子的合数
适用于:最简单、最常用,不要求事先知道关键码的分布
const int MaxSize = 100;
class HashTable1
{
public:
HashTable1( );
~HashTable1( );
int Insert(int k);
int Delete(int k);
int Search(int k);
private:
int H( );
int ht[MaxSize];
};
HashTable1 :: HashTable1( )
{
for (int i = 0; i < MaxSize; i++)
ht[i] = 0;
}
HashTable1 :: ~HashTable1( )
{
}
线性探测法:从冲突位置的下一个位置起,依次寻找空的散列地址。
设散列表的长度为m,对于键值key,发生冲突时,寻找空散列地址的公式为:
Hi=(H(key)+di) % m (di=1,2,…,m-1)
例 1:设关键码集合为 {47, 7, 29, 11, 16, 92, 22, 8, 3},散列表表长为11,散列函数为H(key)=key mod 11,用线性探测法处理冲突,散列表的构造过程如下:
堆积:非同义词对同一个散列地址争夺的现象
算法:Search
输入:闭散列表ht[ ],待查值k
输出:如果查找成功,则返回记录的存储位置,否则返回查找失败的标志-1
1. 计算散列地址 j;
2. 探测下标i初始化:i = j;
3. 执行下述操作,直到 ht[i] 为空:
3.1 若 ht[i] 等于 k,则查找成功,返回记录在散列表中的下标;
3.2 否则,i 指向下一单元;
4. 查找失败,返回失败标志-1;
int HashTable1 :: Search(int k)
{
int i, j = H(k); //计算散列地址
i = j; //设置比较的起始位置
while (ht[i] != 0)
{
if (ht[i] == k) return i; //查找成功
else i = (i + 1) % m; //向后探测一个位置
}
return -1; //查找失败
}
相对于线性探测法,二次探测法能够在一定程度上减少堆积
拉链法如何处理冲突?
对于给定的关键码key执行下述操作:
(1)计算散列地址:j = H(key)
(2)将key对应的记录插入到同义词子表 j 中;
同义词子表:所有散列地址相同的记录构成的单链表。
开散列表:用拉链法处理冲突得到的散列表。
开散列表中存储同义词子表的头指针,开散列表不会出现堆积现象
const int MaxSize = 100;
class HashTable2
{
public:
HashTable2( );
~HashTable2( );
int Insert(int k);
int Delete(int k);
Node<int> * Search(int k);
private:
int H( );
Node<int> * ht[MaxSize];
};
HashTable2 :: HashTable2( )
{
for (int i = 0; i < MaxSize; i++)
{
ht[i] = nullptr;
}
}
HashTable2 :: ~HashTable2( )
{
Node<int> *p = nullptr, *q = nullptr;
for (int i = 0; i < MaxSize; i++)
{
p = q = ht[i];
while (p != nullptr)
{
p = p->next;
delete q;
q = p;
}
}
}
例 1:设关键码集合为 {47, 7, 29, 11, 16, 92, 22, 8, 3},散列表表长为11,散列函数为H(key)=key mod 11,用拉链法处理冲突,散列表的构造过程如下:
j = H(k);
Node<int> *p = ht[j];
while (p != nullptr)
{
if (p->data == k) break;
else p = p->next;
}
if (p == null) {
q = new Node<int>; q->data = k;
q->next = ht[j]; ht[j] = q;
}
Node<int> * HashTable2 :: Search(int k)
{
int j = H(k);
Node<int> *p = ht[j];
while (p != nullptr)
{
if (p->data == k) return p;
else p = p->next;
}
return nullptr;
}
闭散列表:
受数组空间限制,需要考虑存储容量
存储效率较高
开散列表:
没有记录个数的限制,但子表过长会降低查找效率
指针的结构性开销
例 1:设关键码集合{47, 7, 29, 11, 16, 92, 22, 8, 3},散列表表长为 11,散列函数为H(key)=key mod 11,用拉链法处理冲突构造开散列表,删除元素 47。
int HashTable2 :: Delete(int k)
{
int j = H(k);
Node<int> *p = ht[j], *pre = nullptr;
while ((p != nullptr) && (p->data != k)
{
pre = p; p = p->next;
}
if (p != nullptr) {
if (pre == nullptr) ht[j] = p->next;
else pre->next = p->next;
return 1;
} else
return 0;
}
例 2:设关键码集合{47, 7, 29, 11, 16, 92, 22, 8, 3},散列表表长为 11,散列函数为H(key)=key mod 11,用线性探测法处理冲突构造闭散列表,删除元素 47。
顺序查找和其他查找技术相比,缺点是平均查找长度较大,特别是当查找集合很大时,查找效率较低。然而,顺序查找的优点也很突出:算法简单而且使用面广,它对表中记录的存储没有任何要求,顺序存储和链接存储均可应用;对表中记录的有序性也没有要求,无论记录是否有序均可应用。
相对于顺序查找来说,折半查找的查找性能较好,但是它要求线性表的记录必须有序,并且必须采用顺序存储。顺序查找和折半查找一般只能应用于静态查找。
与上述查找技术不同,散列查找是一种基于计算的查找方法,虽然实际应用中关键码集合常常存在同义词,但在选定合适的散列函数后,仅需进行少量的关键码比较,因此,散列技术的查找性能较高。在很多情况下,散列表的空间都比查找集合大,此时虽然浪费了一定的空间,但换来的是查找效率。