目录
查找就是在一组记录集合中找到关键字等于给定值的某个记录或者找到属性值符合条件的某些记录,若表中存在这样的记录表示查找成功,否则,查找失败。
查找分为静态查找和动态查找两种类型。
- 静态查找是指在查找过程中不更改数据集中的数据,主要包括顺序查找法、二分查找法、分块查找法。
- 动态查找是指在查找不成功时将要查找的数据添加到数据集中,包括二叉搜索树、平衡二叉搜索树、B-树、B+树。
- 散列查找:在记录的存储位置和它的关键字中建立一个确定的对应关系。从而在查找的时候根据对应关系就可以确定记录的存储位置,不需要进行关键字的比较。
静态查找
顺序查找
过程:从表中的第一个记录开始,逐个进行记录的关键字和给定值的比较,若某个记录的关键字与给定值相等,则查找成功;否则,直到最后一个记录的关键字与给定值都不等,表明表中没有所查找记录,查找不成功。
template<class T>
int Search(T Array[],T key,int n)
{
for(int i=0;i<n;i++)
{
if(Array[i]==key)
{
return i;
}
}
return -1;
}
如果查找顺序表中每个记录的概率相等,则等概率下查找成功的平均查找长度为 (n+1)/2;
优点:对表的特性没有要求,数据元素可以任意排序,存储可以用数组或者链表。
缺点:平均和最坏情况下时间复杂度都是O(n),数据规模较大时,查找效率低。
二分查找
前提:数据记录有序地存储在线性表中。
基本思想:
- 每次将待查区间中间位置的数据元素和给定值进行比较,若相等则查找成功;
- 若小于给定值,则将查找范围缩小到中间位置的右边区域。
- 若大于给定值,则将查找范围缩小到中间位置的左边区域。
- 在新的区间内重复上述过程,知道查找成功或者查找范围长度为0(查找不成功)为止。
template<class T>
int BiSearch(T Array[], T key, int n)
{
int left = 0, right = n - 1;
int mid;
while (left <= right)
{
mid = (right + left) / 2;
if (Array[mid] == key)
return mid;
else if (key < Array[mid] )
right = mid - 1;
else
left = mid + 1;
}
return -1;
}
假设每个记录的查找概率相等,二分法的平均查找长度为:
优点:二分查找的查找效率比顺序查找高。
缺点:只适用于顺序存储的有序表,有序表中新增或删除时的操作比较复杂。
分块查找:索引顺序查找
在分块查找中,首先将n个数据元素划分为m块(m<=n).每一块中的数据不必有序,但是块与块之间必须“按块有序”:即第一块中任一元素的关键字都必须小于第二块中任一元素的关键字;...第m-1块中任一元素又都必须小于第m块的任一元素。对于每一数据块建立一个索引项,形成具有m个索引项的索引表。索引项中包括:关键字项(块中的最大关键字)和指针项(块中第一个记录的位置)。
分块查找分两个阶段:第一,根据索引表确定查找的记录所在的数据块。可以利用二分查找。第二,顺序查找数据块中的记录。
平均查找长度:索引的平均查找长度+数据块中的平均查找长度。
假设n个记录被分成k块,每块含有m个记录。那么它的平均查找长度为:
动态查找
动态查找是指在查找过程中,如果查找失败,就把待查找的记录插到数据集中。显然动态查找的数据集是通过查找过程而动态生成的。动态查找主要是通过树结构来实现。
在大规模的诗句查找中,大量数据信息存储在外存磁盘。在查找时需要从磁盘中读取数据。磁盘IO时间主要花费在查找时间上了,因此要求数据信息尽量存放在同一盘块,同一磁道上中。如果使用二叉搜索树节点分布在不同的数据块上,导致程序访问时间的增加。但是,利用B-树和B+树可以显著的降低查找磁盘中数据的时间。
B-树
“B-树”有称为B树,是一种平衡的多路查找树。
一棵m阶的B-树,或为空树,或为满足下列特性的m叉树:
- 树中每个结点至多有m棵子树;
- 根节点至少有两棵子树;
- 除跟之外的所有非叶子结点至少有棵子树;
- 所有的叶子结点都出现在同一层;
- 有n棵子树的非叶子结点包含的信息如下:
n为关键字的个数 , 是关键字,且关键字从小到大排序,即 。 是指向子树根节点的指针,且指针 所指子树中所有结点的关键字均小于,所指子树中所有结点的关键字均大于。
性质1:含有n个结点的m阶B树的最大高度
- 查找:与二叉搜索树类似。先从根节点指针开始,将给定值和结点中的关键字 依次进行比较,如果给定值小于,则开始查找所指向的子节点;如果给定值等于,则查找成功;如果给定值大于 ,则查找 ;直到查找的子节点为空,说明查找失败。
- 插入:首先将关键字添加到最底层的某个叶节点中,若该节点不满(关键字个数不超过m-1),则插入完成,否则要将叶节点“分裂”成两个叶子节点,并将一个关键字( )提升到双亲结点中。如果双亲结点已满,则重复刚才的过程,直到到达根节点,并创建新的根节点。
- 删除:首先找到要删除的关键字所在结点;若该节点为最底层的叶子节点,且删除后关键字任然不小于m/2,则删除完成,否则要进行“合并”操作。假如删除的关键字不是叶子节点中的关键字K,则可以将K右边最小的关键字Y代替K,然后将问题转化为从叶节点中删去关键字Y的情形。
B+树-还不够详细
B+树也是一种多叉树结构,通常用于数据库和文件系统中。
B+树是B-树的变形树。在B+树中,所有的数据都存在于叶子节点,内部节点的关键字只是划分子树的分界值。
一棵m阶的B+树,或为空树,或为满足下列特性的m叉树:
- 树中每个结点至多有m棵子树;
- 根节点至少有两棵子树;
- 除跟之外的所有非叶子结点至少有棵子树;
- 所有的叶子结点都出现在同一层;
- 有n棵子树的非叶子结点包含的信息如下:
- 所有的叶子结点和相邻的节点使用链表相连
散列
散列的概念
散列方法就是在记录的关键字和它的存储位置之间建立一个确定的对应函数的关系,使得每个关键字和结构中的一个唯一的存储位置相对应。
散列方法的核心是:由散列函数确定关键字与散列地址之间的对应关系,通过这种关系来实现存储并进行查找。
冲突:不同的关键字由散列函数计算得到的散列地址相同的现象称为“冲突”。
影响散列表查找效率的因素包括:散列函数是否均匀,处理冲突解决的方法以及散列表的装填因子。
装填因子 = 填入表中的元素个数 / 散列表的长度 。
散列函数
假设处理的关键字为整型(非整型可以构建和正整数的对应关系),对于散列函数的要求:
- 散列函数的定义域必须包括全部的关键字。如果散列表包含m个空间时,散列函数的值域必须在[0,m-1];
- 散列函数计算出的地址能够均匀分布在整个地址空间中。
- 简单,便于计算的。
几种散列函数:
- 直接定址法: Hash(key) = a*key+b . 例如有集合{1548,1569,1503,1558} ,hash(key)=key-1500;
- 数字分析法:设有n个d位数,每一位可能有r种不同的符号。可以分析r中符号出现的频率,选取若干位作为散列地址。
- 取余法: Hash(key) = key % p (p<=m) 最接近于或等于m的质数p。
- 平方取中法:先计算关键字的平方值,从而扩大相近数的差别。然后根据散列表长度取中间的几位数作为散列函数值。
- 基数转换法:将关键字转换成另一种进制的数,然后计算散列地址。
- 折叠法:把关键字自左到右分成位数相等的几部分,每一部分位数应与散列表地址位数相同,最后一部分可以短一些。把每一部分的数据叠加起来,就可以得到具有该关键字的记录的散列地址。 1)移位法:把各部分的最后一位对齐相加 2)分界法:
冲突解决
- 开放定址法:"闭散列法"
开放定址法把所有的记录直接存储在散列表中。开放地址法解决冲突的基本思想是:当冲突发生时,使用某种方法为关键字key生成一个探查序列 .其中,即为key的初始探查位置;所有是后继探查位置。找到第一个空闲的位置插入。
1. 线性探查法:
若在初始探查位置 发生冲突,则依次探查 ,直到找到一个空闲的地址插入,或者当探查一遍以后,又回到了地址d,则意味着失败。
在线性探查中,散列地址不同的节点争夺同一个后继,容易产生冲突的一次聚集或堆积。
2. 二次探查法:
为了改善线性探查法的冲突聚集问题,而产生的。
二次探查法的探查序列依次是:
对散列到同一地址的关键字,会产生二次聚集。。。T^T
3. 伪随机探查法:
伪随机探查法是通过一个随机数生成器来生成随机的探查序列,可以防止冲突的“二次聚集”。
随机生成器第i次生成的随机数为,则关键字key的探查地址序列为:
4. 双散列法:
- 链接法:拉链法
在链接法解决冲突的方法中散列表中的每个地址都是一个链表的表头,关联着一个链表结构。散列到相同地址的记录都会放在这个地址关联的链表中。
如果整个散列表存储在内存中,用链接法比较容易实现。但是,如果整个散列表存储在磁盘中,将每个同义词存储在一个新地址的拉链法就不太合适。因为一个同义词链表中的元素可能存储在不同的磁盘块中,这会导致在查询一个特定关键字时多次访问磁盘,从而增加查找时间。
- 桶定址法:
桶定址法的基本思想是把记录分为若干存储桶,每个存储包含一个或多个存储位置,一个存储桶内的各存储位置用指针连接起来。散列函数f将关键字key映射到f(key)号存储桶。如果桶满了可以使用前面介绍的开放定址法来处理。
散列算法设计与分析
#pragma once
//双散列探查法
template<class key,class T>
class HashTable
{
private:
T * HT;
int maxSize;
int currentSize;
int probe(key k, int i)const {
return k%maxSize + i;
}
int hash(key k)const {
return k%maxSize;
}
public:
HashTable(int size)
{
maxSize = size;
currentSize = 0;
HT = new T[maxSize];
}
~HashTable() { delete[] HT; }
bool hashInsert(const T& item)const;
bool hashSearch(const key&k, T&item);
bool hashDelete(const key&k);
};
template<class key,class T>
bool HashTable<key, T>::hashInsert(const T& item)const
{
int home = 0;
int i = 0;
int pos = home = hash(item.getKey());
while (!HT[pos].empty())
{
if (HT[pos] == item)
return false;
i++;
pos = (home + probe(item.getKey(), i)) % maxSize;
}
HT[pos] = item;
std::cout << "insert at " << pos << ": " << item.getKey() << std::endl;
return true;
}
template<class key,class T>
bool HashTable<key, T>::hashSearch(const key& k, T& item)
{
int home = hash(k);
int i = 0;
int pos = home;
while (i<maxSize)//(!HT[pos].empty())
{
if (HT[pos].getKey() == k)
{
item = HT[pos];
return true;
}
i++;
pos = (home + probe(k, i)) % maxSize;
}
return false;
}
template<class key,class T>
bool HashTable<key, T>::hashDelete(const key &k)
{
int home = hash(k);
int i = 0;
int pos = home;
while (i<maxSize)//(!HT[pos].empty())
{
if (HT[pos].getKey() == k)
{
HT[pos] = 0;
return true;
}
i++;
pos = (home + probe(k, i)) % maxSize;
}
return false;
}
应用:通讯录