一、顺序查找
代码简单易懂,但当数据量大的时候,查找效率极为低下,所以该算法适合小量数据。
查找成功最好的情况是在第一个位置就找到了,算法时间复杂度为O(1)
最坏的情况是在最后一个位置就找到了,时间复杂度为O(n)
关键字在任何一个位置的概率是相同的,所以平均查找次数为(n+1) / 2,所以平均时间复杂度为O(n)
从表中第一个或最后一个记录开始,逐个和给定的值比较,如相等则查找成功;如直到最后一个值仍不等时,则表中没有所查的记录,查找不成功。
public static int sequenceSearch(int[] arr, int target) {
if(arr!=null){
for(int i=0;i<arr.length;i++){
if(arr[i]==target){
return i;
}
}
}
return -1;
}
二、二分查找(折半查找)
比较次数少,查找速度快,平均性能好;要求待查数列为有序,且插入删除会带来一定工作量
时间复杂度O(logn)
将数列按序排列,以有序数列的中点位置为比较对象,如果要找的元素值小于该中点元素,则将待查序列缩小为左半部分,否则为右半部分。以此类推不断缩小搜索范围。
public static int binarySearch(int[] arr, int target) {
if (arr != null) {
int min, mid, max;
min = 0;
max = arr.length - 1;
while (min <= max) {
mid = (min + max) / 2;
if (arr[mid] < target) {
min = mid + 1;
} else if (arr[mid] > target) {
max = mid - 1;
} else {
return mid;
}
}
}
return -1;
}
二分查找是性能不错,但为什么一定要折半,而不是折四分之一或者折更多呢?
打个比方,在英文字典里面查‘apple’,你下意识里翻开字典是翻前面的书页还是后面的书页?很显然我们绝对不会从中间查,而是有目的的往前翻。
所以折半查找的mid计算公式mid=(min + max) / 2 可以进行优化:
mid = min + ((target – arr[min])/(arr[max] – arr[min])) * (max – min); // 使用此公式的算法叫插值算法
插值算法适用于表较长,关键字分布均匀的表。
三、哈希查找(散列查找)
哈希查找是通过计算数据元素的存储地址进行查找的一种方法。O(1)的查找,即所谓的秒杀。哈希查找的本质是先将数据映射成它的哈希值。哈希查找的核心是构造一个哈希函数,它将原来直观、整洁的数据映射为看上去似乎是随机的一些整数。
哈希查找的操作步骤:
1)用给定的哈希函数构造哈希表;
2)根据选择的冲突处理方法解决地址冲突;
3)在哈希表的基础上执行哈希查找。
建立哈希表操作步骤:
1) 取数据元素的关键字key,计算其哈希函数值(地址)。若该地址对应的存储空间还没有被占用,则将该元素存入;否则执行step2解决冲突。
2)根据选择的冲突处理方法,计算关键字key的下一个存储地址。若下一个存储地址仍被占用,则继续执行step2,直到找到能用的存储地址为止。
哈希查找步骤为:
1) 对给定k值,计算哈希地址 Di=H(k);若HST为空,则查找失败;若HST=k,则查找成功;否则,执行step2(处理冲突)。
2) 重复计算处理冲突的下一个存储地址 Dk=R(Dk-1),直到HST[Dk]为空,或HST[Dk]=k为止。若HST[Dk]=K,则查找成功,否则查找失败。
哈希函数特点:
①: key尽可能的分散,也就是我丢一个“6”和“5”给你,你都返回一个“2”,那么这样的哈希函数不尽完美。
②:哈希函数尽可能的简单,也就是说丢一个“6”给你,你哈希函数要搞1小时才能给我,这样也是不好的。
哈希函数制定方法:
第一种:直接定址法
取关键字或关键字的某个线性函数值为哈希地址。
H(key)=key或H(key)=a*key+b
其中a和b为常数,这种哈希函数叫做自身函数。
这种函数对于不同的关键字不会发生冲突。
第二种:除法取余法
取关键字被某个不大于哈希表表长m的数p除后所得的余数为哈希地址。
H(key)=k%p
第三种:数字分析法
取关键字中某些取值较分散的若干数字位组成哈希地址。
适合于所有关键字已知,且对每一位的取值做出了一定分析。
第四种:平方取中法
取关键字平方后的中间几位作为哈希地址。
因为一个数平方后的中间几位数和数的每一位都相关,使得散列地址具有较好的分散性,具体取的位数由表长决定。
平方取中法适用于关键字中的每一位取值都不够分散或者较分散的位数小于散列地址所需要的位数的情况。
第五种:摺叠法
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址,这种方法称为摺叠法。
数位叠加时可以有移位叠加和间界叠加(就是把相邻的数镜像叠加)两种方法。
摺叠法适用于关键字的位数较多,而且关键字中每一位上数字分布大致均匀的情况。
第六种:随机数法
选择一个随机函数,取关键字的随机函数值作为它的哈希地址。
H(key)=random(key)
其中random为随机函数。
通常,当关键字长度不等时采用此法。
解决冲突的方法有以下几种:
第一种:开放地址法
开放定址法就是从发生冲突的那个单元开始,按照一定的次序,从散列表中查找出一个空闲存储单元的方法。在这种方法中,散列表的空闲单元不仅向散列地址为对应值的同义词元素开放,而且向发生冲突的其他元素(非同义词元素)开放,此方法的名称由此而来。
从发生冲突的单元起进行查找有多种方法,每一种都对应一定的查找次序或查找路径,产生一个确定的探查序列(即待比较元素的地址序列)。
Hi=(H(key)+di)%m i=1,2,…,k k<=m-1 其中: 为哈希函数;m为哈希表长; 为增量序列,可以有下列取法:
(1)线性探测再散列(线性探查法)
di=1,2,3,…,m-1
线性探测再散列可以保证做到:只要哈希表未填满,总能找到一个不发生冲突的地址。
但是线性探查容易造成堆积现象,因为探查序列过分集中在发生冲突的单元后面,没有在整个散列空间上分散开。
(2)二次探测再散列(平方探查法)
di=1,4,9,16…
(3)伪随机探测再散列
di=伪随机数序列
在处理冲突的过程中有可能会出现两个第一个哈希地址不同的记录争夺同一个后继哈希地址的现象,即在处理同义词的冲突过程中又添加了非同义词的冲突,这种现象称为“二次聚集”,对查找不利。
第二种:再哈希
Hi=RHi(key)
RHi是各自不同的i个哈希函数,即在同义词产生地址冲突时计算另一个哈希函数地址,直到冲突不再发生。这种方法不易产生聚集,但增加了计算的时间。
第三种:链地址法
将哈希值相同的数据元素存放在一个链表中,在查找哈希表的过程中,当查找到这个链表时,采用线性查找方法。
比如说:”5“是一个要保存的数,然后我丢给哈希函数,哈希函数给我返回一个”2″,那么此时的”5“和“2”就建立一种对应关系,这种关系就是所谓的“哈希关系”,在实际应用中也就形成了”2“是key,”5“是value。
哈希函数构造必须要遵守两点原则:
常用的构造哈希函数的方法如下:
哈希函数为“除法取余法”,解决冲突为“开放地址线性探测法”,代码如下:
public class Search {
public static void main(String[] args) {
//“除法取余法”
int hashLength = 13;
int [] array = { 13, 29, 27, 28, 26, 30, 38, 0};
//哈希表长度
int[] hash = new int[hashLength];
//创建hash
for (int i = 0; i < array.length; i++){
insertHash(hash, hashLength, array[i]);
}
int result = searchHash(hash,hashLength,29);
if (result != -1)
System.out.println("已经在数组中找到,索引位置为:" + result);
else
System.out.println("没有此数据");
}
/****
* Hash表检索数据
*
* @param hash
* @param hashLength
* @param key
* @return
*/
public static int searchHash(int[] hash, int hashLength, int key) {
// 哈希函数
int hashAddress = key % hashLength;
// 指定hashAdrress对应值存在但不是关键值,则用开放寻址法解决
while (hash[hashAddress] != 0 && hash[hashAddress] != key) {
hashAddress = (++hashAddress) % hashLength;
}
// 查找到了开放单元,表示查找失败
if (hash[hashAddress] == 0)
return -1;
return hashAddress;
}
/***
* 数据插入Hash表
*
* @param hash 哈希表
* @param hashLength
* @param data
*/
public static void insertHash(int[] hash, int hashLength, int data) {
// 哈希函数
int hashAddress = data % hashLength;
// 如果key存在,则说明已经被别人占用,此时必须解决冲突
while (hash[hashAddress] != 0) {
// 用开放寻址法找到
hashAddress = (++hashAddress) % hashLength;
}
// 将data存入字典中
hash[hashAddress] = data;
}
}
四、索引查找
索引查找是在索引表和主表(即线性表的索引存储结构)上进行的查找。
索引查找的过程是:
1) 首先根据给定的索引值K1,在索引表上查找出索引值等于KI的索引项,以确定对应于表在主表中的开始位置和长度
2) 然后再根据给定的关键字K2,在对应的子表中查找出关键字等于K2的元素(结点)。对索引表或子表进行查找时,若表是顺序存储的有序表,则既可进行顺序查找,也可进行二分查找,否则只能进行顺序查找。
一提到“索引”,估计大家第一反应就是“数据库索引”,对的,其实主键建立“索引”,就是方便我们在海量数据中查找。
实现索引查找时常使用的三个术语:
1)主表:要查找的对象。
2) 索引项:一般我们会用函数将一个主表划分成几个子表,每个子表建立一个索引,这个索引叫做索引项。
3) 索引表:索引项的集合也就是索引表。
一般“索引项”包含三种内容:index,start,length
第一: index,也就是索引指向主表的关键字。
第二:start,也就是index在主表中的位置。
第三:length, 也就是子表的区间长度。
import java.util.Arrays;
public class Search {
// 主表
static int[] students = { 101, 102, 103, 104, 105, 0, 0, 0, 0, 0, 201, 202,
203, 204, 0, 0, 0, 0, 0, 0, 301, 302, 303, 0, 0, 0, 0, 0, 0, 0 };
// 索引表
static IndexItem[] indexItem = { new IndexItem(1, 0, 5), new IndexItem(2, 10, 4), new IndexItem(3, 20, 3), };
// 查找数据
public static int indexSearch(int key) {
IndexItem item = null;
// 建立索引规则
int index = key / 100;
// 首先去索引找
for (int i = 0; i < indexItem.length; i++) {
if (indexItem[i].index == index) {
item = new IndexItem(index, indexItem[i].start, indexItem[i].length);
break;
}
}
// 如果item为null,则说明在索引中查找失败
if (item == null)
return -1;
for (int i = item.start; i < item.start + item.length; i++) {
if (students[i] == key) {
return i;
}
}
return -1;
}
// / 插入数据
public static int insert(int key) {
IndexItem item = null;
// 建立索引规则
int index = key / 100;
int i = 0;
for (i = 0; i < indexItem.length; i++) {
// 获取到了索引
if (indexItem[i].index == index) {
item = new IndexItem(index, indexItem[i].start,
indexItem[i].length);
break;
}
}
if (item == null)
return -1;
// 更新主表
students[item.start + item.length] = key;
// 更新索引表
indexItem[i].length++;
return 1;
}
public static void main(String[] args) {
int value = 205;
// 将205插入集合中,过索引
int index = insert(value);
insert(308);
// 如果插入成功,获取205元素所在的位置
if (index == 1) {
System.out.println("\n插入后数据:" + Arrays.toString(students));
System.out.println("\n数据元素:205在数组中的位置为 " + indexSearch(205) + "位");
}
}
}
// 索引项实体
class IndexItem {
// 对应主表的值
public int index;
// 主表记录区间段的开始位置
public int start;
// 主表记录区间段的长度
public int length;
public IndexItem() {
}
public IndexItem(int index, int start, int length) {
this.index = index;
this.start = start;
this.length = length;
}
}