数据结构与算法(五)-查找算法
1.顺序查找
1.1 数组
顺序查找就是逐一对比,发现有相同的值,就返回下标。
迭代
public int seqSearch(int[] arr, int entry){
int index = 0;
while (index < arr.length){
if(entry == arr[index])
return index;
index++;
}//end while
return -1;
}//end seqSearch
递归
public int seqSearch(int[] arr,int start,int end,int entry){
if(end < start) return -1;
if(entry == arr[start]) return start;
else return seqSearch(arr,start+1,end,entry);
}// end search
顺序查找数组的效率
迭代和递归二者顺序查找比较的次数是一致的
- 最优情况下,在数组的第一个位置找到说要找的项,只进行一次比较,时间复杂度是O(1);
- 最坏情况下,将查找整个数组,在数组的最后找到所需要的项,或者完全没有找到,时间复杂度是O(n);
- 一般的,要查找数组中差不多一半的项,所以平均O(n/2),即平均时间复杂度O(n)。
1.2 链表
迭代
//模仿Map,List中的contains方法
public <T> boolean contains(T entry){
boolean flag = false;
Node curNode = root;//root根节点
while (!flag && (curNode != null)){
if(entry.equals(curNode.getData()))
flag = true;
else curNode = curNode.next;
}//end while
return flag;
}// end contains
递归
public <T> boolean search(Node curNode,T entry){
boolean flag = false;
if(curNode == null) return false;
else if(entry.equals(curNode.getData()))
flag = true;
else
flag = search(curNode.next,entry);
return flag;
}// end search
顺序查找链表的效率
- 最优:O(1);
- 最坏:O(n);
- 平均:O(n)。
2.二分查找
二分查找只适用于有序的数组。
2.1 递归查找
思路分析:
- 确定数组的中间下标
mid = (start + end)/2
; arr[mid]
与目标元素entry
比较arr[mid] == entry
,找到元素,返回下标mid;arr[mid] > entry
,说明要查找的元素在mid左边,递归向左查找;arr[mid] < entry
,说明要查找的元素在mid右边,递归向右查找;
- 递归结束没有找到目标元素,返回-1。
public int binarySearch(int[] arr,int start,int end,int entry){
//可行性判断
if(start > end) return -1;
int mid = start + (end - start) / 2;//不使用mid = (start + end)/2 看下面注释分析
if(arr[mid] == entry) return mid;
else if(arr[mid] > entry)
return binarySearch(arr,start,mid-1,entry);
else
return binarySearch(arr,mid+1,end,entry);
}
2.2 迭代查找
public int binarySearch(int[] arr,int entry){
int start = 0,end = arr.length-1,mid;
while (start <= end){
mid = start + (end - start)/2;
if(arr[mid] == entry) return mid;
if(arr[mid] > entry) end = mid - 1;
else start = mid + 1;
}
return -1;
}
注释:寻找数组的中点,使用
mid = start + (end - start) / 2
来代替mid = (start + end)/2
如果查找至少有
2^30时
,start + end
将超出最大可能的整数值2^31-1
,数值溢出就可能会得到负数,发生异常。另外,我们知道对于电脑来说二进制运算效率高于十进制运算,寻找中点mid还可以写成mid = start + ((end - start)>>1)
,但是它的可读性不强。
2.3 效率
数据量太小或太大都不适合用二分查找。另外,在Java类库中也有定义binarySearch,位于java.util
中的Arrays类中,感兴趣的话可以查看以下API。
2.4 扩展
对于数据量较大时,可以使用插值查找
代码就不写了,就是把二分查找中的mid
获取方式更改一下。
3.分块查找
3.1 算法思想
将原表分成若干块,各块内部不一定有序,但第i
块内所有记录的关键字都小于第i+1
块内所有记录的关键字,这样的表叫做“分块有序”。抽取各块中的最大关键字以及起始位置建立索引表,因为原表是分块有序的,所以所有必定有序。分块查找就是先用二分查找或顺序查找确定待查节点在哪一块,然后在已确定的中进行顺序查找。效率介于顺序查找和二分查找之间。
3.2 算法流程
- 先选取各块中的最大关键字构成一个索引表;
- 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;
- 在已确定的块中用顺序法进行查找。
/**
* 分块查找
*
* @param index
* 索引表,其中放的是各块的最大值
* @param st
* 顺序表,
* @param key
* 要查找的值
* @param m
* 顺序表中各块的长度相等,为m
* @return
*/
public static int blockSearch(int[] index, int[] st, int key, int m) {
// 在序列st数组中,用分块查找方法查找关键字为key的记录
// 1.在index[ ] 中折半查找,确定要查找的key属于哪个块中
int i = binarySearch(index, key);
if (i >= 0) {
int j = i > 0 ? i * m : i;
int len = (i + 1) * m;
// 在确定的块中用顺序查找方法查找key
for (int k = j; k < len; k++)
if (key == st[k]) return k;
}
return -1;
}
时间复杂度:O(log(m)+N/m)。
4.哈希查找
对应的名词有:哈希函数hash(key)、哈希地址、哈希表、哈希地址冲突
哈希查找也叫散列查找,整个散列查找过程分为两步
- 在存储数据时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录;
- 在数据查找时,通过散列函数计算记录的散列地址,然后访问散列地址的记录。
4.1 哈希函数的构造方法
-
直接定址法
取关键字的某个线性函数值为哈希地址,适合用于关键字集合中的值分布连续或基本连续的情况,如果关键字值分布不连续则会造成空间的大量浪费。
-
数字分析法
根据关键字在各个位上的分布情况,选取分布比较均匀的若干位组成哈希地址。适用于处理关键字位数较大的情况。例如记录一个班的学生记录,其关键字为学号,学号的前几位相同,而最后两位或者三位编号分布比较均匀,因此可选择最后的两位作为哈希地址。
-
平方取中法
对关键字平方后,按散列表大小,取中间的若干位作为散列地址。适用于预先不知道关键字的全部情况,取其中哪几位都不一定合适。
-
折叠法
将关键字从左到右分割成位数相等的几个部分,最后一位数可以短些,然后将这几个部分叠加求和,并按哈希表表长,取后几位作为哈希地址。
叠加的方法有两种,一种是移位叠加,即每一段最低为对齐后相加;另一种是间界叠加,即从一端向另一端沿分割界来回折叠对齐后相加。比如关键字是9876543210,散列表表长为三位,我们将它分成四组,987|654|321|0
- 移位叠加:
987+654+321+0=1962
,再求后三位得到散列地址962; - 移位叠加:
987+456+321+0=1764
,再求后三位得到散列地址764。
适合事先不知道关键字分布,关键字位数较多的情况。
- 移位叠加:
-
除留余数法
选择某个适当的整数p,以关键字除以p的余数作为哈希地址。此方法为最常用的构造散列函数的方法。
-
乘余取整法
以关键字乘以常数A,取其小数部分乘以整数B,取其最后的整数部分作为哈希地址。B的选择依赖于哈希表的表长m,A的选择依赖于关键字集合的特征。
-
随机数法
选择一个随机数,取关键字值的随机函数值作为相应记录的哈希地址。
h(key) = random()
。
4.2 处理哈希冲突的方法
-
开放地址法
把哈希表中空位置向向处理地址冲突开放。具体做法是在发生哈希冲突后,从冲突位置开始,使用某种方法在哈希表中形成一个探查序列,找到空地址。
- 线性探查法
-
平方探查法
-
随机探查法
-
再散列函数法
事先准备多几个散列函数。这里的
RHi
就是不同的散列函数 -
链地址法
将哈希地址相同的数据存储到同一个单链表中。即使hashmap就是采用这一的解决方法解决哈希冲突。
4.3 散列表查找算法实现
- 首先定义一个散列表结构
- 对散列表进行初始化
- 对散列表进行插入操作
- 根据不同的情况选择散列函数和处理冲突的方法(这里选用的是除留余数法和链地址法)
public class HashTable<T>{
LinkedList<T> table[];
public HashTable(int len){
//len是哈希表长度,如果len不是素数,则取大于len的最小素数作为哈希表的长度
int np;//大于len的最小素数
if(HashTable.isPrime(len)) np = len;
else {
if(len % 2 == 0) len +=1;
for (np = len;;np++){
if(HashTable.isPrime(np))
break;
}
}
table = new LinkedList[np];
for (int i = 0; i < table.length; i++)
table[i] = new LinkedList<T>();
}
//哈希函数
public int hashCode(T key){
int hc = Math.abs(key.hashCode());
return hc % table.length;//除留余数法
}
//向哈希表中插入元素
public boolean add(T key){
int ha = hashCode(key);
table[ha].add(key);
return true;
}
//删除元素
public boolean remove(T key){
int ha = hashCode(key);
return table[ha].remove(key);
}
//哈希查找
public boolean contains(T key){
int ha = hashCode(key);
return table[ha].contains(ha);
}
//判断n是否为素数
public static boolean isPrime(int n){
if(n < 2) return false;//小于2探查失败
int m = (int)Math.sqrt(n);
for (int i = 2; i < m; i++) //从最小的素数2开始
if(0 == n%i) return false;//能整除则不是素数
return true;
}
}
4.4 哈希查找算法性能分析
对于哈希表查找,关键字比较的次数取决于产生的冲突次数,而影响冲突产生的因素有:
- 哈希函数是否均匀
- 处理冲突的方法
- 哈希表的装载因子。转载因子定义为:表中数据元素个数/表的长度。