算法中有多种查找方法,常见的有:
- 顺序查找:从一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
- 二分查找:在有序的数组中,确定中间的下标mid=(left+right)/2,然后让需要查找的数findVal和arr[mid]比较,若findVal>arr[mid],说明要查找的数在mid的右边,递归向右查找;若findVal<arr[mid],说明要查找的数在mid的左边,递归向左查找;若findVal==arr[mid],说明找到了。
- 插值查找:类似于二分查找,不同的是每次从自适应mid处开始查找。mid=left+(right-left)*(findVal-arr[left])/(arr[right]-arr[left])。适合于数据分布比较均匀的情况。
- 斐波那契查找:利用斐波那契数列将数组拆分为两部分,其中mid=low+F(k-1)-1,F代表斐波那契数列。然后根据findVal和arr[mid]的大小关系进行递归或者循环操作。
- 树表查找:利用树形结构存储数据,根据树的特性进行查找。常见的有二叉排序树、平衡二叉树、B树、B+树等。
- 分块查找:将数组分为若干块,每一块中的元素可以无序,但块与块之间必须有序。先用顺序查找或者二分查找确定待查元素在哪一块中,然后再在该块中用顺序查找或者二分查找确定元素位置。
- 哈希查找:利用哈希函数将数据映射到一个哈希表中,根据哈希值直接访问数据。如果发生哈希冲突,则采用开放定址法或链地址法解决冲突。
以上是我对算法中常见的查找方法的简单介绍,如果你想了解更多细节,请参考以下链接:
3: 七大查找算法(Java版)_解梦者的博客-CSDN博客
4: 算法——查找和排序___矮油不错哟的博客-CSDN博客
算法中的七大查找方法
查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文将介绍算法中常见的七种查找方法,分别是顺序查找、二分查找、插值查找、斐波那契查找、树表查找、分块查找和哈希查找,并给出它们的Java实现示例。
1. 顺序查找
顺序查找是最简单的一种查找方法,也称为线性查找。它的基本思想是从一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。顺序查找适合于存储结构为顺序存储或链接存储的线性表。
顺序查找的时间复杂度为O(n),其中n为线性表的长度。当查找成功时,平均需要比较(n+1)/2次;当查找失败时,需要比较n次。
Java实现示例:
//顺序查找
public static int sequentialSearch(int[] arr, int value) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == value) {
return i; //返回下标
}
}
return -1; //未找到
}
2. 二分查找
二分查找也称为折半查找,是一种有序查找算法。它的基本思想是在有序的数组中,确定中间的下标mid=(left+right)/2,然后让需要查找的数findVal和arr[mid]比较,若findVal>arr[mid],说明要查找的数在mid的右边,递归向右查找;若findVal<arr[mid],说明要查找的数在mid的左边,递归向左查找;若findVal==arr[mid],说明找到了。当left>right时,表示数组中不存在要查找的数。
二分查找的时间复杂度为O(logn),其中n为数组的长度。二分查找要求数组必须是有序的,如果是无序的则要先进行排序操作。对于静态数据集合,一次排序后不再变化,二分查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集合来说,维护有序的排序会带来不小的工作量。
Java实现示例:
//二分查找(非递归)
public static int binarySearch(int[] arr, int value) {
int left = 0; //左指针
int right = arr.length - 1; //右指针
while (left <= right) { //循环条件
int mid = (left + right) / 2; //中间下标
if (arr[mid] == value) {
return mid; //返回下标
} else if (arr[mid] > value) {
right = mid - 1; //向左缩小范围
} else {
left = mid + 1; //向右缩小范围
}
}
return -1; //未找到
}
//二分查找(递归)
public static int binarySearch(int[] arr, int value, int left, int right) {
if (left > right) {
return -1; //递归结束条件
}
int mid = (left + right) / 2; //中间下标
if (arr[mid] == value) {
return mid; //返回下标
} else if (arr[mid] > value) {
return binarySearch(arr, value, left, mid - 1); //递归向左查找
} else {
return binarySearch(arr, value, mid + 1, right); //递归向右查找
}
}
3. 插值查找
插值查找是在二分查找的基础上进行改进的一种查找算法,它的基本思想是根据要查找的数和数组中最大最小值的关系,自适应地选择查找点的位置,从而减少比较次数。插值查找的查找点计算公式为:
mid = left + (right - left) * (value - arr[left]) / (arr[right] - arr[left])
可以看出,当value接近arr[left]时,mid靠近left;当value接近arr[right]时,mid靠近right。这样就可以根据value在数组中的大致位置,快速定位到它所在的区间。然后再根据value和arr[mid]的大小关系,递归或循环地进行查找。
插值查找的时间复杂度为O(log(logn)),其中n为数组的长度。插值查找也要求数组必须是有序的,且数据分布比较均匀。对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。
Java实现示例:
//插值查找(非递归)
public static int interpolationSearch(int[] arr, int value) {
int left = 0; //左指针
int right = arr.length - 1; //右指针
while (left <= right) { //循环条件
int mid = left + (right - left) * (value - arr[left]) / (arr[right] - arr[left]); //插值下标
if (arr[mid] == value) {
return mid; //返回下标
} else if (arr[mid] > value) {
right = mid - 1; //向左缩小范围
} else {
left = mid + 1; //向右缩小范围
}
}
return -1; //未找到
}
//插值查找(递归)
public static int interpolationSearch(int[] arr, int value, int left, int right) {
if (left > right || value < arr[left] || value > arr[right]) {
return -1; //递归结束条件
}
int mid = left + (right - left) * (value - arr[left]) / (arr[right] - arr[left]); //插值下标
if (arr[mid] == value) {
return mid; //返回下标
} else if (arr[mid] > value) {
return interpolationSearch(arr, value, left, mid - 1); //递归向左查找
} else {
return interpolationSearch(arr, value, mid + 1, right); //递归向右查找
}
}
4. 斐波那契查找
斐波那契查找是利用斐波那契数列的特点对有序数组进行分割的一种查找算法。斐波那契数列是指从第三个数开始,每个数都是前两个数的和,例如:1, 1, 2, 3, 5, 8, 13…。斐波那契数列有一个性质:F[n]=F[n-1]+F[n-2],将n-1代入得F[n-1]=F[n-2]+F[n-3],两式相减得F[n]-F[n-1]=F[n-3],即F [n]-1=(F[n-1]-1)+(F[n-2]-1)+1。这个公式说明了可以用斐波那契数列来对数组进行分割。
斐波那契查找的基本思想是先将数组扩展到长度为某个斐波那契数的长度,然后确定中间的下标mid=left+F[k-1]-1,其中k为满足n<=F[k]-1的最小值,n为数组原始长度。然后让需要查找的数findVal和arr[mid]比较,若findVal>arr[mid],说明要查找的数在mid的右边,递归向右查找,并将k减一;若findVal<arr[mid],说明要查找的数在mid的左边,递归向左查找,并将k减二;若findVal==arr[mid],说明找到了。当left>right时,表示数组中不存在要查找的数。
斐波那契查找的时间复杂度为O(logn),其中n为数组的长度。斐波那契查找也要求数组必须是有序的。斐波那契查找与二分查找相比,它可以更灵活地确定分割点位置,从而减少不必要的比较次数。
Java实现示例:
//斐波那契数列
public static int[] fib(int maxSize) {
int[] f = new int[maxSize]; //存放斐波那契数
f[0] = 1;
f[1] = 1;
for (int i = 2; i < maxSize; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f;
}
//斐波那契查找
public static int fibonacciSearch(int[] arr, int value) {
int left = 0; //左指针
int right = arr.length - 1; //右指针
int k = 0; //表示斐波那契分割数值下标
int mid = 0; //存放mid值
int[] f = fib(20); //获取一个斐波那契数列
//获取k值
while (right > f[k] - 1) {
k++;
}
//扩展数组到f[k]大小
int[] temp = Arrays.copyOf(arr, f[k]);
//用最后一个元素填充扩展部分
for (int i = right + 1; i < temp.length; i++) {
temp[i] = arr[right];
}
//循环查找
while (left <= right) {
mid = left + f[k - 1] - 1; //计算mid值
if (value < temp[mid]) { //向左查找
right = mid - 1;
k--; //下次循环mid=left+f[k-1-1]-1
} else if (value > temp[mid]) { //向右查找
left = mid + 1;
k -= 2; //下次循环mid=left+f[k-1-2]-1
} else { //找到了
return Math.min(mid, right); //返回下标,防止越界
}
}
return -1; //未找到
}
5. 树表查找
树表查找是指利用树形结构来存储和查找数据的一种方法。树形结构是一种非线性的数据结构,它由一个根结点和若干个子树组成,每个子树又是一棵树。树形结构具有层次关系和递归特性,可以方便地表示数据之间的一对多的关系。树表查找的优点是查找效率高,插入和删除操作也比较方便。树表查找的缺点是占用空间较大,需要额外的指针域来表示结点之间的关系。
树表查找的常见方法有:二叉排序树查找、平衡二叉树查找、红黑树查找、B树和B+树查找等。这里以二叉排序树查找为例进行介绍。
二叉排序树(Binary Sort Tree)又称为二叉查找树(Binary Search Tree),它或者是一棵空树,或者是具有以下性质的二叉树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树。
二叉排序树查找的基本思想是:首先将给定值k与二叉排序树的根结点的关键字进行比较,若相等则表示查找成功;若k小于该关键字,则在左子树中继续查找;若k大于该关键字,则在右子树中继续查找。重复这一过程,直到找到一个关键字等于k的结点,或者二叉排序树为空,表示查找失败。
二叉排序树查找的时间复杂度为O(h),其中h为二叉排序树的高度。当二叉排序树退化成单支(斜)树时,其最坏时间复杂度为O(n),其中n为结点个数。当二叉排序树是一棵平衡二叉排序树时,其最好时间复杂度为O(logn)。
Java实现示例:
--待补充
6. 分块查找
分块查找又称为索引顺序查找,是一种结合了顺序查找和二分查找的方法。它的基本思想是将一个无序的数组分成若干个大小相等的子块,每个子块内部可以无序,但子块之间必须有序(即第一个子块中任意元素的值都要小于第二个子块中任意元素的值,以此类推)。然后建立一个索引表,存放每个子块中最大(或最小)元素的值和所在位置。当进行查找时,先用二分查找或顺序查找在索引表中确定待查元素属于哪个子块,然后再用顺序查找在相应的子块中进行查找。
分块查找的时间复杂度为O(logn+m),其中n为数组的长度,m为子块的长度。分块查找适合于结构不规则或者难以确定其分布规律的数据集合。分块查找要求划分后的子块有序,但不要求子块内部有序,因此对数据的排序要求不高。分块查找需要额外的空间来存储索引表。
Java实现示例:
//定义索引表结点类
class IndexNode {
int maxKey; //存放每个子块中最大元素的值
int start; //存放每个子块在数组中的起始位置
int end; //存放每个子块在数组中的结束位置
public IndexNode(int maxKey, int start, int end) {
this.maxKey = maxKey;
this.start = start;
this.end = end;
}
@Override
public String toString() {
return "IndexNode [maxKey=" + maxKey + ", start=" + start + ", end=" + end + "]";
}
}
//定义分块查找类
class BlockSearch {
int[] arr; //存放原始数组
IndexNode[] index; //存放索引表
int blockSize; //存放每个子块的大小
public BlockSearch(int[] arr, int blockSize) {
this.arr = arr;
this.blockSize = blockSize;
this.index = createIndex(arr, blockSize); //创建索引表
}
//创建索引表(辅助方法)
public IndexNode[] createIndex(int[] arr, int blockSize) {
int len = arr.length; //数组长度
int num = len % blockSize == 0 ? len / blockSize : len / blockSize + 1; //计算需要划分多少个子块
IndexNode[] index = new IndexNode[num]; //创建索引表数组
for (int i = 0; i < num; i++) { //遍历每个子块
int start = i * blockSize; //计算每个子块在数组中的起始位置
int end = (i + 1) * blockSize - 1 > len - 1 ? len - 1 : (i + 1) * blockSize - 1; //计算每个子块在数组中的结束位置
int maxKey = arr[start]; //假设每个子块中第一个元素为最大值
for (int j = start + 1; j <= end; j++) { //遍历每个子块中的元素
if (arr[j] > maxKey) { //如果发现有更大的值
maxKey = arr[j]; //更新最大值
}
}
index[i] = new IndexNode(maxKey, start, end); //创建索引表结点并赋值
}
return index; //返回索引表数组
}
//查找方法
public int search(int value) {
int i = binarySearch(index, value); //在索引表中用二分查找确定待查元素属于哪个子块
if (i != -1) { //如果找到了
int start = index[i].start; //获取该子块在数组中的起始位置
int end = index[i].end; //获取该子块在数组中的结束位置
for (int j = start; j <= end; j++) { //在该子块中用顺序查找进行查找
if (arr[j] == value) {
return j; //如果找到了,返回下标
}
}
}
return -1; //如果没有找到,返回-1
}
//二分查找(辅助方法)
public int binarySearch(IndexNode[] index, int value) {
int left = 0; //左指针
int right = index.length - 1; //右指针
while (left <= right) { //循环条件
int mid = (left + right) / 2; //中间下标
if (value <= index[mid].maxKey) { //如果待查元素小于等于中间结点的最大值,说明在左边或者就是中间结点
if (mid == 0 || value > index[mid - 1].maxKey) { //如果中间结点是第一个结点,或者待查元素大于前一个结点的最大值,说明就是中间结点
return mid; //返回下标
} else { //否则,说明在左边
right = mid - 1; //向左缩小范围
}
} else { //如果待查元素大于中间结点的最大值,说明在右边
left = mid + 1; //向右缩小范围
}
}
return -1; //未找到
}
}
哈希查找
哈希查找又称为散列查找,是一种根据给定值的特征计算出其在数组中的位置的一种方法。哈希查找的基本思想是:首先定义一个哈希函数(Hash Function),将数组中的每个元素通过哈希函数映射到一个有限的地址空间(Hash Table),形成键值对(Key-Value Pair)。当进行查找时,先用哈希函数将给定值转换为相应的地址,然后在哈希表中进行匹配,如果匹配成功,则表示查找成功;如果匹配失败,则表示查找失败。
哈希查找的时间复杂度为O(1),即常数时间。哈希查找适合于数据量大且分布均匀的情况。哈希查找的优点是查找速度快,不受数据规模影响。哈希查找的缺点是需要额外的空间来存储哈希表,且哈希函数的设计比较复杂,需要避免冲突(Collision)的发生。冲突是指不同的元素经过哈希函数后得到相同的地址,这会影响查找效率和准确性。常见的解决冲突的方法有:开放地址法(Open Addressing)、链地址法(Chaining)、再哈希法(Rehashing)等。
Java实现示例:
//定义哈希表结点类
class HashNode {
int key; //存放元素值
int value; //存放元素在数组中的下标
HashNode next; //指向下一个结点
public HashNode(int key, int value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "HashNode [key=" + key + ", value=" + value + "]";
}
}
//定义哈希表类
class HashTable {
int size; //存放哈希表的大小
HashNode[] table; //存放哈希表数组
public HashTable(int size) {
this.size = size;
this.table = new HashNode[size]; //创建哈希表数组
}
//定义哈希函数(辅助方法)
public int hash(int key) {
return key % size; //简单地取模作为哈希函数
}
//插入元素
public void insert(int key, int value) {
int index = hash(key); //计算元素经过哈希函数后得到的地址
HashNode node = new HashNode(key, value); //创建新结点
if (table[index] == null) { //如果该地址为空,直接插入
table[index] = node;
} else { //如果该地址不为空,说明发生了冲突,采用链地址法解决冲突
HashNode temp = table[index]; //获取该地址上的结点
while (temp.next != null) { //遍历链表,直到最后一个结点
temp = temp.next;
}
temp.next = node; //将新结点插入到链表尾部
}
}
//查找元素
public int search(int key) {
int index = hash(key); //计算元素经过哈希函数后得到的地址
if (table[index] == null) { //如果该地址为空,说明没有找到
return -1;
} else { //如果该地址不为空,遍历链表进行匹配
HashNode temp = table[index]; //获取该地址上的结点
while (temp != null) { //循环条件
if (temp.key == key) { //如果找到了,返回下标
return temp.value;
}
temp = temp.next; //继续查找下一个结点
}
return -1; //如果遍历完链表还没有找到,说明没有找到
}
}
}
结论
本文介绍了算法中常见的七种查找方法,它们各有优缺点,适用于不同的场景和需求。在实际应用中,需要根据数据的特点和规模,选择合适的查找方法,以提高查找效率和准确性。以下是对这七种查找方法的一个简单的总结和比较:
查找方法 | 时间复杂度 | 空间复杂度 | 数据要求 | 优点 | 缺点 |
---|---|---|---|---|---|
顺序查找 | O(n) | O(1) | 无序 | 简单 | 慢 |
二分查找 | O(logn) | O(1) | 有序 | 快 | 需要排序 |
插值查找 | O(log(logn)) ~ O(n) | O(1) | 有序,分布均匀 | 对于均匀分布的数据更快 | 需要排序,对于不均匀分布的数据效果不好 |
斐波那契查找 | O(logn) | O(1) | 有序 | 比二分查找更节省空间,适合于静态数据结构 | 需要排序,需要预存斐波那契数列 |
树表查找 | O(h) ~ O(n) | O(n) | 无序或有序 | 查找效率高,插入和删除操作方便,可以表示一对多的关系 | 占用空间较大,需要额外的指针域 |
分块查找 | O(logn+m) | O(n/m) + m | 无序或有序,子块有序 | 对数据的排序要求不高,适合于结构不规则或者难以确定其分布规律的数据集合 | 需要额外的空间来存储索引表 |
哈希查找 | O(1) ~ O(n) | O(n) + m (m为哈希表大小) | 无序或有序,分布均匀或不均匀 | 查找速度快,不受数据规模影响,可以处理任意类型的数据 | 需要额外的空间来存储哈希表,哈希函数的设计比较复杂,需要避免冲突 |