查找问题

(1)顺序查找(Sequential Search)

对数组进行遍历,跟每个元素进行比较

【复杂度分析】查找成功时的平均查找长度为:(假设每个数据元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2;当查找不成功时,需要n+1次比较,时间复杂度为O(n)。所以,顺序查找的时间复杂度为O(n)

int SequenceSearch(int a[], int value, int n){
    int i;
    for(i=0; i<n; i++)
        if(a[i]==value)
            return i;
    return -1;
}

(2)二分查找(Binary Search)

前提元素必须是有序,二分查找是有序数组中用到的较为频繁的一种算法

【复杂度分析】最坏情况下,关键词比较次数为log_2(N+1),且期望时间复杂度为O(log_2N)

譬如数组{1, 2, 3, 4, 5, 6, 7, 8, 9},查找元素6,用二分查找的算法执行的话,其顺序为:
1.第一步查找中间元素,即5,由于5<6,则6必然在5之后的数组元素中,那么就在{6, 7, 8, 9}中查找;
2.寻找{6, 7, 8, 9}的中位数,为7,7>6,则6应该在7左边的数组元素中,那么只剩下6,即找到了。
二分查找算法就是不断将数组进行对半分割,每次拿中间元素和goal进行比较。

(1)求有序表的中间位置mid;
(2)若r[mid].key==k,查找成功;
    若r[mid].key>k,在左子表中继续进行二分查找;
    若r[mid].key<k,则在右子表中继续进行二分查找。

注意】折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。

#include <iostream>
using namespace std;
//递归二分查找
int binarysearch(int vector[], int x, int low, int high)
{
  int mid;
  if(low>high)
    return -1;
  mid=(low+high)/2;
  return (x==vector[mid]?mid:x<vector[mid]?
  		binarysearch(vector,x,low,mid-1):
		binarysearch(vector,x,mid+1,high));
}
//递归二分查找
int binarySearch2(int a[], int value, int low, int high)
{
    int mid = low+(high-low)/2;
    if(a[mid]==value)
        return mid;
    if(a[mid]>value)
        return BinarySearch2(a, value, low, mid-1);
    if(a[mid]<value)
        return BinarySearch2(a, value, mid+1, high);
}
//循环二分查找
int binary_search(int* a, int len, int goal)
{
    int low = 0;
    int high = len - 1;
    while(low <= high)
    {
        int middle = (low + high)/2;
        if(a[middle] == goal)
            return middle;
        //在左半边
        else if(a[middle] > goal)
            high = middle - 1;
        //在右半边
        else
            low = middle + 1;
    }
    //没找到
    return -1;
}
int main()
{
   int vector[10]={1,2,3,4,5,6,7,8,9,10};
   cout<<binarysearch(vector,7,0,10); //返回7在数组中的位置
   const int LEN=10000;
   int i,a[LEN];
   for(i=0;i<LEN;i++)
        a[i]=i-5000;
    int goal=0;
    int index=binary_search(a,LEN,goal);
    if(index!=-1)
        cout<<goal<<"在数组中的下标为"<<binary_search(a,LEN,goal)<<endl;
    else
        cout<<"不存在"<<goal<<endl;
   return 0;
}

(3)插值查找

插值查找的不同点在于:自适应,每一次并不是从中间切分,而是根据离所求值的距离进行搜索的,比如要在取值范围1 ~ 10000 之间 100 个元素从小到大均匀分布的数组中查找5, 我们自然会考虑从数组下标较小的开始查找。

二分查找中查找点计算如下:

  mid=(low+high)/2, 即mid=low+1/2*(high-low);

插值查找中查找点计算如下:

  mid=low+(key-a[low])/(a[high]-a[low])*(high-low),即将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值更新得更靠近关键字key,这样也就间接地减少了比较次数。

【基本思想】基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,插值查找也属于有序查找。

【注意】对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。

【复杂度分析】查找成功或者失败的时间复杂度均为O(log_2(log_2N))。

int InsertionSearch(int a[], int value, int low, int high){
    int mid = low+(value-a[low])/(a[high]-a[low])*(high-low);
    if(a[mid]==value)
        return mid;
    if(a[mid]>value)
        return InsertionSearch(a, value, low, mid-1);
    if(a[mid]<value)
        return InsertionSearch(a, value, mid+1, high);
}

(4)斐波那契查找

斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….

随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618

【基本思想】也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。

斐波那契查找类似于折半查找,其根据斐波那契序列的特点对有序表进行分割。要求开始表中记录的个数为比某个斐波那契数小1的值,即N=F(k)-1;

开始将key值与第F(k-1)位置的记录进行比较(即mid=low+F(k-1)-1),比较结果也分为三种

  1)相等,mid位置的元素即为所求;

  2)>,low=mid+1,k-=2,【说明】low=mid+1说明待查找的元素在[mid+1,high]范围内,k-=2说明范围[mid+1,high]内的元素个数为N-(F(k-1))= F(k)-1-F(k-1)=F(k)-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找;

  3)<,high=mid-1,k-=1,【说明】low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归的应用斐波那契查找。

复杂度分析】最坏情况下,时间复杂度为O(log_2N),且其期望复杂度也为O(log_2N)。

#include<string.h> 
#include<iostream>
using namespace std;
const int max_size=20;
void Fibonacci(int * F){
    F[0]=0;
    F[1]=1;
    for(int i=2;i<max_size;++i)
        F[i]=F[i-1]+F[i-2];
}
//定义斐波那契查找法,a为要查找的数组,n为要查找的数组长度,key为要查找的关键字
int FibonacciSearch(int *a, int n, int key){
  int low=0;
  int high=n-1;
  int F[max_size];
  Fibonacci(F);//构造一个斐波那契数组F 
  int k=0;
  while(n>F[k]-1)//计算n位于斐波那契数列的位置
      ++k;
  int * temp;//将数组a扩展到F[k]-1的长度
  temp=new int [F[k]-1];
  memcpy(temp,a,n*sizeof(int));
  for(int i=n;i<F[k]-1;++i)
     temp[i]=a[n-1];
  while(low<=high){
    int mid=low+F[k-1]-1;
    if(key<temp[mid]){
      high=mid-1;
      k-=1;
    }
    else if(key>temp[mid]){
     low=mid+1;
     k-=2;
    }
    else{
       if(mid<n)
           return mid;//若相等则说明mid即为查找到的位置
       else
           return n-1;//若mid>=n则说明是扩展的数值,返回n-1
    }
  }  
  delete [] temp;
  return -1;
}
int main()
{
    int a[] = {0,16,24,35,47,59,62,73,88,99};
    int key=100;
    int index=FibonacciSearch(a,sizeof(a)/sizeof(int),key);
    cout<<key<<" is located at:"<<index;
    return 0;
}

(5)分块查找

分块查找又称索引顺序查找,它是顺序查找的一种改进方法。

基本思想】将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
step1 先选取各块中的最大关键字构成一个索引表;
step2 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。

(6)哈希查找

哈希表:每个元素的关键字都与一个函数值(即数组下标)相对应(直接定址+解决冲突)

哈希函数/散列函数:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。

基本思想】如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。

  1)用给定的哈希函数构造哈希表;

  2)根据选择的冲突处理方法解决地址冲突,常见的解决冲突的方法:拉链法(Separate Chaining)和线性探测法(Linear Probing);

  3)在哈希表的基础上执行哈希查找。

哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。

复杂度分析】单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。

(7)树表查找

二叉查找树平均查找性能不错,为O(logn),但是最坏情况会退化为O(n)。在二叉查找树的基础上进行优化,我们可以使用平衡查找树。平衡查找树中的2-3查找树,这种数据结构在插入之后能够进行自平衡操作,从而保证了树的高度在一定的范围内进而能够保证最坏情况下的时间复杂度。但是2-3查找树实现起来比较困难,红黑树是2-3树的一种简单高效的实现,他巧妙地使用颜色标记来替代2-3树中比较难处理的3-node节点问题。红黑树是一种比较高效的平衡查找树,应用非常广泛,很多编程语言的内部实现都或多或少的采用了红黑树。

除此之外,2-3查找树的另一个扩展——B/B+平衡树,在文件系统和数据库系统中有着广泛的应用。

  • 二叉树查找(BST)

【基本思想】二叉查找树(BinarySearch Tree)/二叉搜索树/二叉排序树(Binary Sort Tree)是先将待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围。 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。 

二叉查找树或是一棵空树,或是具有下列性质的二叉树:

  1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

  2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

  3)任意节点的左、右子树也分别为二叉查找树。

对二叉查找树进行中序遍历,即可得到有序的数列。

【复杂度分析】同二分查找一样,插入和查找的时间复杂度均为O(logN),但是在最坏的情况下仍会有O(N)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡(如查找下图(b)中的“93”,需要进行N次查找操作)。期望在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。

  •   平衡查找树:2-3查找树(2-3 Tree)查找

不同于二叉树,2-3树运行每个节点保存1个或者两个的值。对于普通的2节点(2-node),他保存1个key和左右两个自己点。对应3节点(3-node),保存两个Key,2-3查找树的定义如下:

  1)要么为空,要么:

  2)对于2节点,该节点保存一个key及对应value,以及两个指向左右节点的节点,左节点也是一个2-3节点,所有的值都比key要小,右节点也是一个2-3节点,所有的值比key要大;

  3)对于3节点,该节点保存两个key及对应value,以及三个指向左中右的节点。左节点也是一个2-3节点,所有的值均比两个key中的最小的key还要小;中间节点也是一个2-3节点,中间节点的key值在两个跟节点key值之间;右节点也是一个2-3节点,节点的所有key值比两个key中的最大的key还要大。

【性质】1)如果中序遍历2-3查找树,就可以得到排好序的序列;2)在一个完全平衡的2-3查找树中,根节点到每一个为空节点的距离都相同。(这也是平衡树中“平衡”一词的概念,根节点到叶节点的最长距离对应于查找算法的最坏情况,而平衡树中根节点到叶节点的距离都一样,最坏情况也具有对数复杂度。)

复杂度分析】2-3树的查找效率与树的高度是息息相关的:在最坏的情况下,也就是所有的节点都是2-node节点,查找效率为lgN;在最好的情况下,所有的节点都是3-node节点,查找效率为log3N约等于0.631lgN。对于距离来说:对于1百万个节点的2-3树,树的高度为12-20之间,对于10亿个节点的2-3树,树的高度为18-30之间。对于插入来说:只需要常数次操作即可完成,因为他只需要修改与该节点关联的节点即可,不需要检查其他节点,所以效率和查找类似。

  •  平衡查找树:红黑树(Red-Black Tree)查找

2-3查找树能保证在插入元素之后能保持树的平衡状态,最坏情况下即所有的子节点都是2-node,树的高度为lgn,从而保证了最坏情况下的时间复杂度。但是2-3树实现起来比较复杂,于是就有了一种简单实现2-3树的数据结构,即红黑树(Red-Black Tree)。

【基本思想】红黑树的思想就是对2-3查找树进行编码,尤其是对2-3查找树中的3-nodes节点添加额外的信息。红黑树中将节点之间的链接分为两种不同类型,红色链接,他用来链接两个2-nodes节点来表示一个3-nodes节点。黑色链接用来链接普通的2-3节点。特别的,使用红色链接的两个2-nodes来表示一个3-nodes节点,并且向左倾斜,即一个2-node是另一个2-node的左子节点。这种做法的好处是查找的时候不用做任何修改,和普通的二叉查找树相同。

红黑树是一种具有红色和黑色链接的平衡查找树,同时满足:1)红色节点向左倾斜;2)一个节点不可能有两个红色链接;3)整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同。

下图可以看到红黑树其实是2-3树的另外一种表现形式:如果我们将红色的连线水平绘制,那么他链接的两个2-node节点就是2-3树中的一个3-node节点了:

【性质】整个树完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同。

【复杂度分析】最坏的情况就是,红黑树中除了最左侧路径全部是由3-node节点组成,即红黑相间的路径长度是全黑路径长度的2倍;红黑树的平均高度约为lgN。

下图是一个典型的红黑树,从中可以看到最长的路径(红黑相间的路径)是最短路径的2倍:

红黑树是2-3查找树的一种实现,它能保证最坏情况下仍然具有对数的时间复杂度。其应用十分广泛,在多种编程语言中被用作符号表的实现,如:Java中的java.util.TreeMap,java.util.TreeSetC++ STL中的:map,multimap,multiset.NET中的:SortedDictionary,SortedSet 等。

  • B树/B+树(B Tree/B+ Tree)查找

平衡查找树中的2-3树中,一个节点最多有2个key,而红黑树则使用染色的方式来标识这两个key。

B树(B-tree)是一种树状数据结构,它能够存储数据、对其进行排序并允许以O(logN)的时间复杂度进行查找、顺序读取、插入和删除的数据结构。B树,概括来说是一个节点可以拥有多于2个子节点的二叉查找树。与平衡二叉查找树不同,B树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。普遍运用于数据库文件系统

B树可以看作是对2-3查找树的一种扩展,即其允许每个节点有M-1个子节点。

  1. 根节点至少有两个子节点;

  2. 每个节点有M-1个key,并且以升序排列;

  3. 位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间;

  4. 其它节点至少有M/2个子节点。

例如一个M=4 的4阶B树:

可以看到B树是2-3树的一种扩展,其允许一个节点有多于2个的元素,其插入及平衡化操作和2-3树很相似。例如,往B树中依次插入

6 10 4 14 5 11 15 3 2 12 1 7 8 8 6 3 6 21 5 15 15 6 32 23 45 65 7 8 6 5 4 的演示动画:

B+树是对B树的一种变形树,它与B树的差异在于:

  1. 有k个子结点的结点必然有k个关键码;
  2. 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中;
  3. 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。

例如下图为一个B+树及其插入的演示动画:

B和B+树的区别在于,B+树的非叶子结点只包含导航信息,不包含实际的值,所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。

B+树的优点1)由于B+树在内部节点上不好含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子几点上关联的数据也具有更好的缓存命中率;2)B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。

B树的优点:由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。

B 树和B+树的区别
B 树和B+树的区别标题

B/B+树常用于文件系统和数据库系统中,它通过对每个节点存储个数的扩展,使得对连续的数据能够进行较快的定位和访问,能够有效减少查找时间,提高存储的空间局部性从而减少IO操作。它广泛用于文件系统及数据库中,如:1)Windows:HPFS文件系统;2)Mac:HFS,HFS+文件系统;3)Linux:ResiserFS,XFS,Ext3FS,JFS文件系统;4)数据库:ORACLE,MYSQL,SQLSERVER等中。

猜你喜欢

转载自blog.csdn.net/u013228808/article/details/83067681