一、线性表的查找
1. 顺序查找(线性查找)
1.1 基本概念
顺序查找的过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表示没有所查的记录,查找不成功。
假设每个数据元素的查找概率相等,则查找成功时的平均查找长度为A
S
L
=
1
n
(
1
+
2
+
3
+
…
+
n
)
=
n
+
1
2
ASL=frac1n (1+2+3+…+n)=frac{n+1} 2
ASL=n1(1+2+3+…+n)=2n+1;由此可见,顺序查找的时间复杂度为
O
(
n
)
O(n)
O(n)。
1.2 代码
#include
using namespace std;
// 全局变量 查找表
#define LEN (10)
int searchTable[LEN + 1] = { 0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 }; // 查找表元素从下标1开始存储
/**
* @brief: 常规顺序查找
* @param a: 顺序表指针
* @param n: 顺序表长度
* @param key: 要查找的关键字
* @return: 若查找成功,则返回key所在下标,否则返回0
*/
int sequentialSearch(int* a, int n, int key)
{
for (size_t i = 1; i n; i++)
{
if (a[i] == key)
{
return i; // 查找成功
}
}
return 0; // 查找失败
}
int main(int argc, char* argv[])
{
int key = 199;
int pos = sequentialSearch(searchTable, LEN, key);
if (pos)
{
cout "the index of " key " is " pos endl;
}
else
{
cout "not found" endl;
}
}
从上面代码可以看出,除了要进行
a
[
i
]
a[i]
a[i]与关键字
k
e
y
key
key是否相等比较外,每次循环还需要对
i
i
i是否越界进行检查。为了避免每次循环的越界检查,可以设置一个哨兵,即设置
a
[
0
]
=
k
e
y
a[0]=key
a[0]=key,优化后的代码如下。
#include
using namespace std;
// 全局变量 查找表
#define LEN (10)
int searchTable[LEN + 1] = { 0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 }; // 查找表元素从下标1开始存储
/**
* @brief: 带哨兵优化的顺序查找
* @param a: 顺序表指针
* @param n: 顺序表长度
* @param key: 要查找的关键字
* @return: 若查找成功,则返回key所在下标,否则返回0
*/
int sequentialSearchOpt(int* a, int n, int key)
{
a[0] = key; // 哨兵
int i = n; // 循环从尾部开始
while (a[i] != key)
{
i--;
}
// 由于设置了a[0]=key,所以即使在a[1]~a[n]处不等于key,那么while循环也会在i=0时结束,此时查找失败
return i;
}
int main(int argc, char* argv[])
{
int key = 199;
int pos = sequentialSearchOpt(searchTable, LEN, key);
if (pos)
{
cout "the index of " key " is " pos endl;
}
else
{
cout "not found" endl;
}
}
2. 折半查找(二分或对分查找)
2.1 基本概念
折半查找的前提是线性表中的记录必须是有序的,线性表必须采取顺序存储。折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录相等,则查找成功;若给定值小于中间记录,则在中间记录的左半区继续查找;若给定值大于中间记录,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。
折半查找的时间复杂度为l
o
g
(
n
)
log(n)
log(n)。
2.2 代码
#include
using namespace std;
// 全局变量 查找表
#define LEN (10)
int searchTable[LEN + 1] = { 0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 }; // 查找表元素从下标为1处开始
/**
* @brief: 二分查找迭代形式
* @param a: 查找表指针
* @param n: 查找表长度
* @param key: 要查找的关键字
* @return: 若查找成功,则返回key所在下标,否则返回0
*/
int binarySearch(int* a, int n, int key)
{
int low = 1; // 记录最低下标
int high = n; // 记录最高下标
int mid; // 记录中值下标
while (low high)
{
mid = (low + high) / 2;
if (key > a[mid]) // 若查找值大于中值
{
low = mid + 1;
}
else if (key a[mid]) // 若查找值小于中值
{
high = mid - 1;
}
else // 若查找值等于中值
{
return mid;
}
}
return 0;
}
int main(int argc, char* argv[])
{
int key = 59;
int pos = binarySearch(searchTable, LEN, key);
if (pos)
{
cout "the index of " key " is " pos endl;
}
else
{
cout "not found" endl;
}
}
#include
using namespace std;
// 全局变量 查找表
#define LEN (10)
int searchTable[LEN + 1] = { 0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 }; // 查找表元素从下标为1处开始
/**
* @brief: 二分查找递归形式
* @param a: 查找表指针
* @param n: 查找表长度
* @param key: 要查找的关键字
* @return: 若查找成功,则返回key所在下标,否则返回0
*/
int binarySearchRec(int* a, int low, int high, int key)
{
if (low > high)
{
return 0;
}
int mid = (low + high) / 2;
if (key > a[mid]) // 若查找值大于中值
{
return binarySearchRec(a, mid + 1, high, key);
}
else if (key a[mid]) // 若查找值小于中值
{
return binarySearchRec(a, low, mid - 1, key);
}
else // 若查找值等于中值
{
return mid;
}
}
int main(int argc, char* argv[])
{
int key = 59;
int pos = binarySearchRec(searchTable, 1, LEN, key);
if (pos)
{
cout "the index of " key " is " pos endl;
}
else
{
cout "not found" endl;
}
}
3. 插值查找
3.1 基本概念
插值查找本质上也是一种二分查找,只不过二分出来的两个有序表长度一大一小。
先看二分查找的mid计算公式:
m
i
d
=
l
o
w
+
h
i
g
h
2
=
l
o
w
+
1
2
(
h
i
g
h
−
l
o
w
)
mid=frac{low+high}2=low+frac12 (high-low)
mid=2low+high=low+21(high−low)
即它的mid等于最低下标low加上最高下标high与low的差的一半。
插值查找就是将这个1/2进行了改进,具体计算如下:
m
i
d
=
l
o
w
+
k
e
y
−
a
[
l
o
w
]
a
[
h
i
g
h
]
−
a
[
l
o
w
]
(
h
i
g
h
−
l
o
w
)
mid=low+frac{key-a[low]}{a[high]-a[low]}(high-low)
mid=low+a[high]−a[low]key−a[low](high−low)
其核心为在于插值公式
k
e
y
−
a
[
l
o
w
]
a
[
h
i
g
h
]
−
a
[
l
o
w
]
frac{key-a[low]}{a[high]-a[low]}
a[high]−a[low]key−a[low]
插值查找的时间复杂度为l
o
g
(
n
)
log(n)
log(n)。
3.2 代码
#include
using namespace std;
// 全局变量 查找表
#define LEN (10)
int searchTable[LEN + 1] = { 0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 }; // 查找表元素从下标为1处开始
/**
* @brief: 插值查找
* @param a: 查找表指针
* @param n: 查找表长度
* @param key: 要查找的关键字
* @return: 若查找成功,则返回key所在下标,否则返回0
*/
int insertSearch(int* a, int n, int key)
{
int low = 1;
int high = n;
int mid;
while (low high)
{
mid = low + (key - a[low]) / (a[high] - a[low]) * (high - low); // 插值
if (key > a[mid])
{
low = mid + 1;
}
else if (key a[mid])
{
high = mid - 1;
}
else
{
return mid;
}
}
return 0;
}
int main(int argc, char* argv[])
{
int key = 59;
int pos = insertSearch(searchTable, LEN, key);
if (pos)
{
cout "the index of " key " is " pos endl;
}
else
{
cout "not found" endl;
}
}
4. 斐波拉契查找
4.1 基本概念
斐波拉契查找与插值查找的思想一样,也是把查找表分成两个长度不同的有序表,只不过计算mid的方式不同,斐波拉契查找是基于斐波拉契数列来确定mid值。
斐波拉契数列是这样的一个数列:
F
(
n
)
=
1
,
1
,
2
,
3
,
5
,
8
,
13
,
21
,
34
,
…
F(n)={1,1,2,3,5,8,13,21,34,…}
F(n)=1,1,2,3,5,8,13,21,34,…
它满足这样的规律:
F
(
n
)
=
{
1
n
=
0
,
1
F
(
n
−
2
)
+
F
(
n
−
1
)
n
⩾
2
F(n)=begin{cases} 1 & n=0,1 \ F(n-2)+F(n-1) & ngeqslant2 \ end{cases}
F(n)={1F(n−2)+F(n−1)n=0,1n⩾2
随着n越来越大,F(n-1)与F(n)的比值越来越接近于黄金分割比例0.618,故斐波拉契查找又称为黄金分割查找。
斐波拉契查找的前提要求有序查找表记录数n为某个斐波拉契数减1,即
n
=
F
(
k
)
−
1
n=F(k)-1
n=F(k)−1,这样mid可将查找表分成左右长度分别为
F
(
k
−
1
)
−
1
F(k-1)-1
F(k−1)−1和
F
(
k
−
2
)
−
1
F(k-2)-1
F(k−2)−1的两个有序表。至于为什么要求
n
=
F
(
k
)
−
1
n=F(k)-1
n=F(k)−1,可移步至下面的参考链接内容,对照图示理解。
4.2 代码
#include
#include
using namespace std;
// 全局变量,斐波拉契数列
vectorint> FibArray = { 1, 1 }; // 初始第一、二项
vectorint> searchTable = { 0, 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 }; // 查找表元素从下标为1处开始
/**
* @brief: 斐波拉契查找
* @param a: 查找表 vector
* @param key: 要查找的关键字
* @return: 若查找成功,则返回key所在下标,否则返回0
*/
int fibonacciSearch(vectorint>& a, int key)
{
int n = a.size() - 1; // 查找表长度,有效元素从下标1开始
int low = 1; // 记录最低下标
int high = n; // 记录最高下标
int mid; // 记录中值下标
int k = 0;
while (n > FibArray[k] - 1) // 计算n位于斐波拉契数列的位置
{
k++;
if (k >= FibArray.size())
{
FibArray.push_back(FibArray[k - 1] + FibArray[k - 2]);
}
}
for (size_t i = n; i FibArray[k] - 1; i++) // 查找表起初长度可能不足 FibArray[k] - 1,将其补足
{
a.push_back(a[n]);
}
while (low high)
{
mid = low + FibArray[k - 1] - 1; // 分隔点mid位置
if (key a[mid]) // 若查找值大于中值
{
high = mid - 1;
k -= 1;
}
else if (key > a[mid]) // 若查找值小于中值
{
low = mid + 1;
k -= 2;
}
else
{
if (mid n)
{
return mid;
}
else // 若mid>n说明是补全的数值,直接返回n
{
return n;
}
}
}
return 0;
}
int main(int argc, char* argv[])
{
int key = 59;
int pos = fibonacciSearch(searchTable, key);
if (pos)
{
cout "the index of " key " is " pos endl;
}
else
{
cout "not found" endl;
}
}
二、树表的查找
1. 基本概念
二叉排序树(Binary Sort Tree)又称为二叉搜索树、二叉查找树,其定义如下:
二叉排序树或是空树,或是满足如下性质的二叉树:
(1)若其左子树非空,则左子树上所有结点的值均小于根结点的值;
(2)若其右子树非空,则右子树上所有结点的值均大于等于根结点的值;
(3)其左右子树本身又各是一棵二叉排序树。
二叉排序树的性质:
中序遍历非空的二叉排序树所得到的数据元素序列是一个按关键字排列的递增有序序列。
1.1 二叉排序树操作——查找
- 若查找的关键字等于根结点,成功
- 否则
- 若小于根结点,查其左子树
- 若大于根结点,查其右子树
- 在左右子树上操作类似
二叉排序树上查找某关键字等于给定值的结点过程,其实就是走了一条从根到该结点的路径。
比较次数=该结点所在层数
最多比较次数=树的深度
含有n个结点的二叉排序树的平均查找长度和树的形态有关,最好情况是该二叉排序树是一个完全二叉树,此时查找的时间复杂度为O
(
l
o
g
n
)
O(logn)
O(logn),最坏情况是该二叉排序树是个斜树,此时查找的时间复杂度为
O
(
n
)
O(n)
O(n)。
1.2 二叉排序树操作——插入
- 若二叉排序树为空,则插入结点作为根结点插入到空树中
- 否则
- 树中已有,不再插入
- 树中没有
- 查找直至某个叶子结点的左子树或右子树为空为止,则插入结点应为该叶子节点的左孩子或右孩子。
插入的元素一定在叶结点上。
1.3 二叉排序树操作——生成
从空树出发,经过一系列的查找、插入操作之后,可生成一棵二叉排序树。
一个无序序列可通过构造二叉排序树而变成一个有序序列,构造树的过程就是对无序序列进行排序的过程。
插入的结点均为叶子结点,故无需移动其他结点,相当于在有序序列上插入记录而无需移动其他记录。
但是,关键字的输入顺序不同,建立的二叉排序树形态也不同。
1.4 二叉排序树操作——删除
从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去,只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变。
由于中序遍历二叉排序树可以得到一个递增有序的序列。那么,在二叉排序树中删除一个结点相当于删去有序序列中的一个结点。
- 将因删除结点断开的二叉链表重新链接起来
- 防止重新链接后的树的高度增加
二叉排序树删除结点的三种情况
- 被删除的结点是叶子结点:直接删去该结点。
- 被删除的结点只有左子树或者只有右子树:用其左子树或者右子树替换它(结点替换)。
- 被删除的结点既有左子树,也有右子树:
- 以其中序前驱值替换之(值替换),然后再删除该前驱结点。前驱是左子树中最大的结点;
- 以其中序后继值替换之(值替换),然后再删除该后继结点。后继是右子树中最小的结点;
2. 平衡二叉树
三、散列表的查找
1. 基本概念
基本思想:记录的存储位置与关键字之间存在对应关系
对应关系——hash函数
L
o
c
(
i
)
=
H
(
k
e
y
i
)
Loc(i) = H(key_{i})
Loc(i)=H(keyi)
优点:查找效率高
缺点:空间效率低
散列表的若干术语:
- 散列方法(杂凑法):选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放;查找时,由同一个函数对给定值k计算地址,将k与地址单元中元素关键码进行对比,确定查找是否成功;
- 散列函数(杂凑函数):散列方法中使用的转换函数;
- 散列表(杂凑表)按上述思想构造的表;
- 冲突:不同的关键码映射到同一个散列地址
k
e
y
1
≠
k
e
y
2
key_{1} ≠ key_{2}
key1=key2,但是
H
(
k
e
y
1
)
=
H
(
k
e
y
2
)
H(key_{1}) = H(key_{2})
H(key1)=H(key2)
- 同义词:具有相同函数值的多个关键字。
使用散列表要解决好两个问题:
- 构造好的散列函数
(a)所选函数尽可能简单,以便提高转换速度;
(b)所选函数对关键码计算出的地址,应在散列地址集中均匀分布,以减少空间浪费。- 制定一个好的解决冲突的方案
查找时,如果从散列函数计算出的地址中查不到关键码,则应当依据解决冲突的规则,有规律地查询其它相关单元。
2. 散列函数的构造方法
2.1 直接定址法
H
a
s
h
(
k
e
y
)
=
a
⋅
k
e
y
+
b
(
a
、
b
为常数
)
Hash(key) = a·key + b (a、b为常数)
Hash(key)=a⋅key+b (a、b为常数)
优点:以关键码
k
e
y
key
key的某个线性函数值为散列地址,不会产生冲突;
缺点:要占用连续地址空间,空间效率低。
2.2 除留余数法
H
a
s
h
(
k
e
y
)
=
k
e
y
m
o
d
p
(
p
是一个整数
)
Hash(key) = key mod p (p是一个整数)
Hash(key)=key mod p (p是一个整数)
关键:如何选取合适的
p
p
p?
技巧:设表长为
m
m
m,取
p
⩽
m
p leqslant m
p⩽m 且为质数
3. 冲突处理方法
3.1 开放地址法(开地址法)
基本思想:有冲突时就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将数据元素存入。例如:
除留余数法:H
i
=
(
H
a
s
h
(
k
e
y
)
+
d
i
)
m
o
d
m
(
d
i
为增量序列
)
H_{i} = (Hash(key) + d_{i}) mod m (d_{i}为增量序列)
Hi=(Hash(key)+di) mod m (di为增量序列)
常用方法:
- 线性探测法:
d
i
d_{i}
di为1,2,…,m-1线性序列
- 二次探测法:
d
i
d_{i}
di为
1
2
1^2
12,
−
1
2
-1^2
−12,
2
2
2^2
22,
−
2
2
-2^2
−22,…,
q
2
q^2
q2线性序列
- 伪随机探测法:
d
i
d_{i}
di为伪随机数序列
3.2 链地址法(拉链法)
基本思想:相同散列地址的记录链成一单链表。
m
m
m个散列地址就设
m
m
m个单链表,然后用一个数组将
m
m
m个单链表的表头指针存储起来,形成一个动态的结构。
优点:
- 非同义词不会冲突,无“聚集”现象;
- 链表上的结点空间动态申请,更适合于表长不确定的情况。
4. 查找性能分析
平均查找长度ASL取决于
- 散列函数
- 处理冲突的方法
- 散列表的装填因子
α
α
α
=
表中填入的记录数
哈希表的长度
α = frac{表中填入的记录数}{哈希表的长度}
α
α
A
S
L
ASL
ASL与装填因子
α
α
α有关!既不是严格的
O
(
1
)
O(1)
O(1),也不是
O
(
n
)
O(n)
O(n):
A
S
L
≈
1
+
α
2
(
拉链法
)
ASL ≈ 1 + frac{α}{2} (拉链法)
ASL≈1+2α (拉链法)
A
S
L
≈
1
2
(
1
+
1
1
−
α
)
(
线性探测法
)
ASL ≈ frac{1}{2}(1 + frac{1}{1 – α}) (线性探测法)
ASL≈21(1+1−α1) (线性探测法)
A
S
L
≈
−
1
α
l
n
(
1
−
α
)
(
随机探测法
)
ASL ≈ -frac{1}{α}ln(1 – α)(随机探测法)
ASL≈−α1ln(1−α)(随机探测法)
5. 代码
#include
#include
using namespace std;
#define HASHSIZE (5) // 散列表表长,自定义
struct HashNode {
int key; // 关键字
HashNode* next; // 指向下一个结点
HashNode() {
key = -1;
next = nullptr;
};
HashNode(int key) : key(key), next(nullptr) {};
};
/* 哈希表结构 */
struct HashTable {
HashNode** elem; // 哈希表数组,每个元素都是一个指向HashNode结构的指针(每个链表的首地址)
};
/**
* @brief: 散列表初始化
* @param H: 散列表指针
* @return:
*/
void InitHashTable(HashTable *H)
{
H->elem = new HashNode*[HASHSIZE]; // 分配哈希表数组空间
for (size_t i = 0; i HASHSIZE; i++)
{
H->elem[i] = nullptr;
}
}
/**
* @brief: 散列表销毁,回收空间
* @param H: 散列表指针
* @return:
*/
void DestroyHashTable(HashTable* H)
{
// 对每个链表的每个结点元素进行回收
for (size_t i = 0; i HASHSIZE; i++)
{
HashNode* root;
HashNode* tmp;
root = H->elem[i];
while (root)
{
tmp = root->next;
delete root;
root = tmp;
}
}
delete[] H->elem;
}
/**
* @brief: 散列函数:除留余数法
* @param key: 关键字
* @return: 散列值
*/
int Hash(int key)
{
return key % HASHSIZE;
}
/**
* @brief: 将关键字插入散列表
* @param H: 散列表指针
* @param key: 关键字
* @return:
*/
void InsertHash(HashTable *H, int key)
{
int addr = Hash(key); // 散列值,应该放到散列表的哪个链表上
HashNode* node = new HashNode(key);
// 头插法
node->next = H->elem[addr];
H->elem[addr] = node;
}
/**
* @brief: 散列表查找
* @param H: 散列表指针
* @param key: 关键字
* @return: true/false
*/
bool SearchHash(HashTable* H, int key)
{
int addr = Hash(key); // 散列值,应该在散列表的哪个链表上查找
HashNode* root = H->elem[addr]; // 链表头结点
while (root)
{
if (key == root->key) // 找到记录
{
return true;
}
root = root->next;
}
return false;
}
int main()
{
HashTable* H = new HashTable;
// 初始化哈希表
InitHashTable(H);
// 往哈希表中插值
vectorint> searchTable = { 1, 16, 24, 35, 47, 59, 62, 73, 88, 99 }; // 查找表元素
for (size_t i = 0; i searchTable.size(); i++)
{
InsertHash(H, searchTable[i]);
}
// 哈希表查找
int key = 98;
if (SearchHash(H, key))
{
cout "found" endl;
}
else
{
cout "not found" endl;
}
// 销毁散列表
DestroyHashTable(H);
delete H;
return 0;
}
参考链接
青岛大学数据结构-王卓
七大查找算法
查找算法—(图解)斐波那契查找
斐波拉契查找法的原理及实现
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net