查找算法总结

来源:互联网 发布:知乎300txt 编辑:程序博客网 时间:2024/06/09 17:26

查找是在大量的信息中寻找一个特定的信息,在计算机中查找是非常常用的一种运算。在这里将总结下学习过的查找算法。

静态查找和动态查找:静态查找和动态查找都是针对查找表而言,动态查找是指查找表中有删除和插入操作。
有序查找和无序查找:无序查找对于被查找数列的排序没有要求,而有序查找要求被查找数列必须有序。
常见的查找算法:顺序查找(O(n)),二分查找(数组必须是有序的,O(logn),二分查找的改进版本:),二叉树查找(二叉树的改进版本:2-3查找树和红黑树),散列值查找。

一 顺序查找:

思想:这是最简单也是毫无技巧的一种查找方式,就是对于被查找数列,从左向右遍历所有的元素,并依次和查找元素比较,如果成功匹配就返回相应的值,否则返回查找失败的标志。对于数组是否有序没有强制要求,如果数据有序,遍历数据组比较到命中或者大于改查找元素而结束,对于无序数组,则遍历完所有元素,直至命中或者全部元素遍历结束。
效率:O(n)
实现代码:

package Search;import java.util.Random;public class Sequential {    public static void show(int[] a) {        for (int temp : a)            System.out.print(temp + " ");    }    public static int search(int[] a, int key) { //无序数组        for (int i = 0; i < a.length; i++)            if (a[i] == key)                return i; // 查找命中,则返回查找元素第一次出现的位置。        return a.length; // 查找未命中,则返回待搜索数组的长度    }    public static int search_sort(int[] a, int key) { //递增数组        for (int i = 0; i < a.length; i++)            if (a[i] == key)                return i; // 查找命中,则返回查找元素第一次出现的位置。            else if(a[i]>key)                 break;        return a.length; // 查找未命中,则返回待搜索数组的长度    }    public static void main(String[] args) {        int N = 10;        int[] a = new int[N];        Random random = new Random();        for (int i = 0; i < N; i++)            a[i] = random.nextInt(20);        show(a);        System.out.println( );        int key=random.nextInt(20);        System.out.println(key+"'s position:"+search(a, key));    }}

二 折半查找:

折半查找(二分查找)方法针对的是有序数组的查找,整个查找思路就是将待查找元素和被查找的(子)数组中间位置的元素进行比较,如果相等,则命中,如果不相等,则根据比较结果和该位置右边或者左边的子数组的中间位置的元素进行比较,直至命中,或者查找未命中结束。
优化思想:
1) 相较于二分查找,每次都是从固定的点开始划分查找边界(即查找子表的中间位置),也就是说折半查找这种查找方式不是自适应的(是傻瓜式的)。基于二分查找算法,可以将查找点的选择改为自适应的,根据关键字在整个有序表所处的位置,让mid值的变化更加靠近关键字key,这样就间接的减少了比较的次数,这种改进的方法的关键就是mid的值的计算:
mid=lo+(key-a[lo])/(a[hi]-a[lo])*(hi-lo)
这种优化思路所带来的查找算法又被称为插值查找(但是对于查找表长较大,而分布比较均匀的查找表来说,插值查找算法比二分查找算法好太多。反之,如果数组中的数据分布非常不均匀,那么插值查找算法并不是一个好的选择)。
2)利用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。这种查找又被称作斐波那契查找(斐波那契数是指 f1=1,f2=1,fn=fn-1+fn-2,(1,1,2,3,5,8,13,21….)随着斐波那契数的递增,,前后两个数的壁纸会越来越接近0.618)。斐波那契查找和二分查找非常类似,只不过是每次查找点的算则不同,它也是一种有序查找,但是这种查找算法使用非常局限,他要求被查找的有序数组大小必须是某个斐波那契数值-1(f(k)-1)。如果不是,必须扩展该数组才能使用斐波那契查找算法。开始将查找值和第f(k-1)的位置进行比较,比较结果也分成三类:
mid=lo+f(k-1)-1
a)key=a[mid]: 那么mid位置的元素就是所求的元素的位置;
b)key>a[mid]: lo=mid+1;k=k-2;
新的待查找范围为(mid+1,hi)
n-(mid+1)=f(k)-1-(f(k-1)-1+1)=f(k)-f(k-1)-1=f(k-2)-1(恰好是某个斐波那契数-1),可以递归的使用斐波那契查找
c) key

package Search;import java.util.Random;public class Binary_Search {    public static void show(int[] a) {        for (int temp : a)            System.out.print(temp + " ");    }    private static int search(int[] a, int key) {//非递归版本        int lo =0, hi = a.length - 1;        while (hi >= lo) {            int mid = lo + (hi - lo) / 2;            if (a[mid] == key)                return mid;            else if (a[mid] > key)                hi = mid - 1;            else if (a[mid] < key)                lo = mid + 1;        }        return a.length;    }    private static int searchone(int[] a,int key){        return search_re(a,key,0,a.length-1);    }    private static int search_re(int[] a, int key, int lo, int hi) {// (尾)递归的方法        if (hi < lo)            return a.length;        int mid = lo + (hi - lo) / 2;        if (a[mid] == key)            return mid;        else if (a[mid] > key)            return search_re(a, key, lo, mid - 1);        else if (a[mid] < key)            return search_re(a, key, mid + 1, hi);        return a.length;    }    public static void main(String[] args) {        int N = 10;        int[] a = new int[N];        Random random = new Random();        for (int i = 0; i < N; i++)            a[i] = random.nextInt(20);        BasicSort.Merge.sort(a);        show(a);        System.out.println();        int key = random.nextInt(20);        System.out.println(key + "'s position:" + searchone(a, key));    }}

三 二叉树查找:

四 散列值查找:

如果所有的键都是比较小的整数,我们可以用一个数组来实现符号表,将键的值作为数组的索引而数组中索引值为i处存储的就是的值就是i,这样就可以快速的访问任意键的值(如果构建好了符号表,查找耗时是O(1))。而散列值查找(哈希查找)正是这种简易的方法的扩展并且能够处理更加复杂的类型的键。这种查找算法是典型的在时间和空间上做出权衡的经典的算法,如果没有内存限制,我们可以直接将键值作为索引号,那么所有的查找操作只需要访问内存一次就可以实现,但是这样所需的内存太大,特别是对于键值比较大的查找。
散列值查找为两个步骤:
1) 利用散列函数将被查找的键转换为数组的一个索引(理想情况系,不同的键都能转换成不同的索引,但是我们也会遇到两个或者多个键被散列到相同的索引位置,这就是hash冲突)
2) 解决hash冲突,介绍两种经典的处理碰撞冲突的方法:拉链法和线性探测法。

1.散列函数:

这个过程会将键值换成数组的索引,如果我们有一个大小为M的数组,那么我们就需要一个能将任意的值转换成该数组范围内的索引(0~M-1)的散列函数,而且我们要寻找的散列函数应该易于计算并且能够达到较为均匀分配所有的键。散列函数和键值的类型有关。
1) 正整数:将正整数散列最常用的方法是除留余数法。我们选择大小为素数M的数组,对于任何正整数K计算K除以M的余数,这个实现非常容易,并且能够有效地将键值散列在0~M-1的区间内。(如果M不是素数,我们可能不能有效的利用键中包含的信息,将会导致我们无法均匀的散列所有的键值)。
2) 浮点数:如果键值是0-1之间的浮点数,我们可以将它乘以M并四舍五入得到一个0~M-1之间的索引(但是这种做法非常有缺陷,这样高位的键的作用相对于低位的键的作用更大,有一个优化的思想就是先将键值转换成二进制,在使用除数留余法。
3) 字符串:除数留余法也可以使用在字符串上,我们只需要将其当做较大的整数即可。一种叫做Horner的典型算法用来处理字符串键的散列(相当于把字符串当做一个N位的R进制的值,将它除以M并取余),只要R足够小,不造成溢出,就可以选择适当的M将字符串散列到0~M-1)。

int hash=0;for(int i=0;i<s.length;i++){hash=(R*hash+s.charAt(i))%M;}

4)组合键:如果键的类型包含了多个整型变量,我们就可以像String类型那样将他们混合起来。
ex:日期

int hash=(((day*R+month))%M)*R+year)%M

在Java中,Java令所有的数据类都继承了一个能够返回32bit整数的hashCode()方法,而我们需要的不是32位的整数,而是数组索引,在实现中,我们会将默认的hashCode()方法和除数留余法相结合。

private int hash(Key key){return (key.hashCode()&0x7fffffff)%M}

软存储:如果散列值的计算很耗时,我们可以将每个键的散列值缓存起来,即在每个键中使用一个hash变量来存储它的hashCode( )的返回值。第一次调用hashCode( )方法的时候,我们需要计算对象的散列值,但是之后对于hashCode的计算会直接调用hash变量(java中的String的hashCode( )就是使用了这种方法)。

2.处理hash碰撞:

也就是处理两个或者多个键的散列值相同的情况,常用的两种方法拉链法和线性探测。
1)基于拉链法:
拉链法:是一种相对直接的处理hash碰撞的方法,就是将大小为M的数组中的每个元素都指向一个链表,链表中的每个结点都存储了散列值为该元素的索引的键值(键值对),这种方法被称为拉链法,因为发生冲突的元素都被存储在了链表中。这种方法的基本思想就是选择足够大的M,使得链表都尽可能短,从而保证高效的查找,这样查找被分成两部分,一部分是根据查询值计算出它的散列值并找到对应的链表,然后使用顺序查找法查找对应链表中相应的元素。
在实现基于拉链法的散列表的时候,目标是选择合适的数组大小M的值,既不会因为空链表而浪费空间,也不会由于链表太长而在查找上浪费太多时间。而其实对于拉链法来说,这并不是关键的选择,如果存入的键值多余预期,查找的时间只会比选择更大的数组稍慢些,如果少于预期,虽然会浪费空间,但是查找会非常快。
散列表主要的目的是在于均匀地将键值散布开来,因此在计算散列值后键的顺序信息就丢失了,如果你要快速的找到最大的或者最小的元素相较于二叉树类的查找,散列表都不是好的选择,因此这些操作在散列表中都是线性的。
优化思想:
对于拉链法,只有保证在拉链法中保持较短的链表(平均长度最好在2-8之间),如果在拉链法中,能够准确的估计用例所需的散列表的大小,调整数组的工作并不是必须的,只要根据耗时和(1+N/M)成比例来选取适当的M。
实现代码:

2)线性探测法:
实现散列表的另外的一种方式就是用大小为M的数组保存N个键值对,其中M>N。我们需要依靠数组中的空位解决散列冲突,基于这种策略的所有方法被统称为开放地址散列表。
开放地址散列表中最简单的方法叫做线性探测法:当碰撞发生的时候,我们直接检查散列表中下一个位置(索引+1)这样的线性探测可能产生3种情况:
a) 命中,该位置的键和查找的键相同。
b) 未命中,键为空;
c) 继续查找,该位置的键和查找的键不相同。
也就是,用散列函数计算键在数组的索引位置,检查其中的键和被查找键是否相同,不为空且不相同的时候,继续查找(将索引增大,到达数组尾部的时候折回数组头部)直到找到该键或者是遇到一个空元素。(在开放地址的散列表中的删除操作不能简单直接将找到的待删除元素的索引位置的元素直接置为null,如果这样操作了,在下次删除或者查找新的元素的时候就有可能遇到问题,因为null在开放地址散列表中意味着查找失败并且结束。)
优化思想:
对于开放地址的散列表(在这里指的是线性探测法)可以调整数组的大小的方法保证散列表的使用率永远不会超过1/2在最开始构造数组的时候,接受一个固定的容量作为参数,创造数组。在需要调整数组大小的时候,创建一个新的给定大小的数组,保存原表中的key和value值,然后将原表中的数据重新散列并插入到新的数组中。这样我们可以使得数组的大小得到调整,在put()数据的时候,调用这个方法保证,散列表最多处于半满状态。

实现代码:

五单词查找树:

0 0
原创粉丝点击