查找:在一些数据元素中,通过一定的方法找出与给定关键字相同的数据元素的过程。
线性表的查找、树表的查找、散列表的查找
列表查找(线性表查找):从列表中查找指定元素
- 输入:列表、待查找元素
- 输出:元素下标(未找到元素时一般返回None或-1)
内置列表查找函数:index()–线性查找
1. 顺序查找(Linear Search)
-
顺序查找:也叫线性查找,从列表第一个元素开始,顺序进行搜索,知道找到元素或搜索到列表最后一个元素为止。
-
时间复杂度:O(n)
-
空间复杂度:O(1)
Python代码实现
# 方式1:
def linear_search(lis, val):
for i in range(len(lis)):
if val == lis[i]:
return iC
return False
# 方式2:
def linear_search(lis, val):
for ind, v in enumerate(lis):
if val == v:
return ind
return None
lis = [1,2,3,4,5]
print(linear_search(lis, 5))
C++代码实现
#include <iostream>
using namespace std;
int Linear_find(int *pArr, int length, int val)
{
for (int i = 0; i <= length; i++)
{
if (val == pArr[i])
{
return i;
}
}
return -1;
}
int main1(void)
{
int arr[] = { 5, 3, 2, 4, 1 };
int val = 4;
int length = sizeof(arr) / sizeof(arr[0]);
cout << val << "数组中的位置为:" << Linear_find(arr, length, val) << endl;
system("pause");
return 0;
}
2. 二分查找(Binary Search)–前提:要排序
二分查找:又叫折半查找,从有序列表的初始候选区li[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半
- 时间复杂度:O(logn)
- 空间复杂度:O(1)
Python代码实现
# 方式1:
def binary_search(lis, val):
left = 0
right = len(lis) - 1
while left <= right:
mid = (left + right) // 2
if lis[mid] == val:
return mid
elif lis[mid] > val:
right = mid - 1
else:
left = mid + 1
return None
# 方式2:
def binary_search(lis, left, right, val):
mid = (left + right) // 2
while left <= right:
if lis[mid] == val:
return mid
elif lis[mid] > val:
return binary_search(lis, left, mid-1, val)
else:
return binary_search(lis, mid+1, right, val)
return None
lis = [1,2,3,4,5,6,7]
print(binary_search(lis, 0, 6, 1))
C++代码实现
#include <iostream>
using namespace std;
int binary_find(int pArr[], int length, int val)
{
int left = 0;
int right = length-1;
while (left <= right)
{
int mid = (left + right) / 2;
if (val == pArr[mid])
return mid;
else if (val > pArr[mid])
left = mid + 1;
else
right = mid - 1;
}
return NULL;
}
int binary_search(int pArr[], int left, int right, int val)
{
int mid = (left + right) / 2;
cout << "left:" << left << ", right:" << right << ", mid:" << mid << endl;
while (left <= right)
{
if (val == pArr[mid])
{
cout << "val=" << val << ", mid=" << pArr[mid] << endl;
return mid;
}
else if (val > pArr[mid])
return binary_search(pArr, mid + 1, right, val);
else
return binary_search(pArr, left, mid - 1, val);
}
return NULL;
}
int main(void)
{
int arr[5] = { 1,2,3,4,5 };
int length = sizeof(arr) / sizeof(arr[0]);
int val = 5;
cout << binary_search(arr, 0, length - 1, val) << endl;
system("pause");
return 0;
}
如果只是查找一次,选择顺序查找即可,但是如果多次,可以先对list进行排序,然后查找,效率高,但就针对一次进行排序查找的效果不如顺序查找。
3. 分块查找
分块查找,又称为索引顺序查找,吸取了顺序查找和折半查找各自的优点,既有动态结构,又适合快速查找。
基本思想:将查找表分为若干个子块。块内元素可以无序,但块之间是有序的,即第一个块中的最小关键字小于第二个块中的所有记录的关键字,第二个块中的最大关键字小于第三个块中的所有记录的关键字,以此类推。在建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块中第一个元素的地址,索引表按关键字有序排列。
算法流程:
先选取各块中的最大关键字构成一个索引表;
查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。
不准确的评价:
- 时间复杂度:O(log(n/s)+s) 二分查找+线性查找
- 空间复杂度:O(2s) 索引表
三种方法对比
顺序查找 | 二分查找 | 分块查找 |
---|---|---|
ASL | 最大 | 最小 |
表结构 | 有序表、无序表 | 有序表 |
存储结构 | 顺序表、线性链表 | 顺序表 |
树表查找
二叉排序树
二叉排序树(Binary Sort Tree)又称为二叉搜索树、二叉查找树
定义:
二叉排序树或是空树,或是满足如下性质的二叉树:
- 若其左子树非空,则左子树上所有结点的
值均小于根结点的值; - 若其右子树非空,则右子树上所有结点的
值均大于等于根结点的值; - 其左右子树本身又各是一棵二叉排序树
中序遍历二叉排序树:
中序遍历非空的二叉排序树所得到的数据元素序列是一个按关键字排列的递增有序序列
二叉排序树的递归查找【算法思想】
- 若二叉排序树为空,则查找失败,返回空指针。
- 若二叉排序树非空,将给定值key与根结点的关键字
T->data.key进行比较:
- 若key等于T->data.key,则查找成功,返回根结点地址;
- 若key小于T->data.key,则进一步查找左子树;
- 若key大于T->data.key,则进一步查找右子树。
struct TreeNode{
int val;
TreeNode* left;
TreeNode* right;
TreeNode() : val(-1), left(nullptr), right(nullptr){}
TreeNode(int x) : val(x), left(nullptr), right(nullptr){}
TreeNode(int x, TreeNode* left, TreeNode* right) : val(-1), left(left), right(right){}
};
class TreeSearch{
public:
bool treefind(TreeNode* root, int val){
if (root == nullptr) return false;
else if (root->val == val) return true;
else if (val > root->val) return treefind(root->right, val);
else if (val < root->val) return treefind(root->left, val);
}
};
二叉排序树的查找分析
二叉排序树上查找某关键字等于给定值的结点过程,其实就是走了一条从根到该结点的路径。
比较的关键字次数 = 此结点所在层次数
最多的比较次数 = 树的深度
二叉排序树的平均查找长度:
含有n个结点的二叉排序树的平均查找长度和树的形态有关
最好的情况(形态比较均衡): O ( l o n g 2 n ) O(long_2n) O(long2n)
最坏的情况(退化为顺序查找): O ( n ) O(n) O(n)
如何提高形态不平衡的二叉排序树的查找效率?
解决办法:做==“平衡化”==处理,即尽量让二叉树的形状均衡 --平衡二叉树
平衡二叉树
平衡二叉树(balance binary tree)
- 又称为AVL树(Adelson-Velskii and Landis)
- 一颗平衡二叉树或者是空树,或者是具有下列性质的二叉排序树:
- 左子树与右子树的高度之差的绝对值小于等于1
- 左子树和右子树也是平衡二叉排序树
为了方便起见,给每个结点附加一个数字,给出该结点左子树与右子树的高度差。这个数字称为结点的平衡因子(BF) 。
平衡因子=结点左子树的高度–结点右子树的高度
根据平衡二叉树的定义,平衡二叉树上所有结点的平衡因子只能是-1、0或1。
对于一棵有n个结点的AVL树,其高度保持在
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)数量级,ASL也
保持在
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)量级
失衡二叉排序树的分析与调整
当我们在一个平衡二叉排序树上插入一个结点时,有可能导致失衡,即出现平衡因子绝对值大于1的结点,如:2、-2。
如果在一棵AVL树中插入一个新结点后造成失衡,则必须重新调整树的结构,使之恢复平衡。
平衡调整的四种类型:
LL调整过程:
RR调整过程
LR型调整过程
RL型调整
输入关键字序列(16,3,7,11,9,26,18,14,15),构建AVL树的步骤
补充
二叉排序树的操作–插入:
- 若二叉排序树为空,则插入结点作为根结点插入到空树中
- 否则,继续在其左、右子树上查找
- 树中已有,不再插入
- 树中没有
- 查找直至某个叶子结点的左子树或右子树为空为止,则插入结点应为该叶子结点的左孩子或右孩子
插入元素一定是在叶子结点上
二叉排序树的操作–生成:
从空树出发,经过一系列的查找、插入操作之后,可生成一棵二叉排序树
一个无序序列可通过构造二叉排序树而变成一个有序序列。构造树的过程就是对无序序列进行排序的过程。
插入的结点均为叶子结点,故无需移动其他结点。相当于在有序序列上插入记录而无需移动其他记录。
二叉排序树的操作–删除:
从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去,只能删掉该结点,并且还应保证删除后所得的二叉树仍然满足二叉排序树的性质不变。
由于中序遍历二叉排序树可以得到一个递增有序的序列。那么,在二叉排序树中删去一个结点相当于删去有序序列中的一个结点。
- 将因删除结点而断开的二叉链表重新链接起来
- 防止重新链接后树的高度增加
- 被删除的结点是叶子结点:直接删去该结点
其双亲结点中相应指针域的值改为“空”
- 被删除的结点只有左子树或者只有右子树,用其左子树或者右子树替换它(结点替换)。
其双亲结点的相应指针域的值改为“指向被删除结点的左子树或右子树”
- 被删除的结点既有左子树,也有右子树
以其中序前趋值替换之(值替换),然后再删除该前趋结点。前趋是左子树中最大的结点。
也可以用其怎继替换之,然后再删除该后继结点。(后继是右子树中最小的结点。
散列表的查找
基本思想:记录的存储位置与关键字之间存在对应关系
对应关系----hash函数
散列表
散列方法(杂凑法)
选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放;查找时,由同一个函数对给定值k计算地址,将k与地址单元中元素关键码进行比,确定查找是否成功。
散列函数(杂凑函数):散列方法中使用的专函函数
冲突:不同关键码映射到同一个散列地址
同义词:具有相同函数值得多个关键字
使用散列表要解决好两个问题:
- 构造好的散列函数
(a)所选函数尽可能简单,以便提高转换速度
(b)所选函数对关键码计算出的地址,应在散列地址集中致均匀分布,以减少空间浪费。
构造散列函数考虑的因素
①执行速度(即计算散列函数所需时间)
②关键字的长度
③散列表的大小;关键字的分布情况
④查找频率
根据元素集合的特性构造
要求一:n个数据原仅占用n个地址,虽然散列查找是以空间换时间,但仍希望散列的地址空间尽量小。
要求二:无论用什么方扶存储,目的都是尽量均匀地存放元素,以避免冲突。
- 直接定址法
- 数字分析法
- 平方取中法
- 折叠法
- 除留余数法
- 随机数法
处理冲突的方法
- 开放定址法(开地址法)
- 链地址法
- 再散列法(双散列函数法)
- 建立一个公共溢出区
链地址法的优点:
- 非同义词不会冲突,无“聚集”现象
- 链表上结点空间动态申请,更适合于表长不确定的情况
结论
散列表技术具有很好的平均性能,优于一些传统的技术
链地址法优于开地址法
除留余数法作散列函数优于其它类型函数