返回顶部

『嗨威说』数据结构中常用的查找算法思路总结

 本文基本索引目录

 一、查找的基本概念和专业术语

二、顺序查找算法  

三、二分查找算法

四、二叉排序树算法  

五、平衡二叉树算法

六、B树简介  

七、散列表查找

 

 一、查找的基本概念和专业术语

  (1)查找:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。

  (2)查找算法分类:

  1)静态查找和动态查找;
    注:静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。

    常见静态表:顺序查找、二分查找、插值查找、斐波那契查找、线性索引查找

    常见动态表:二叉排序树、平衡二叉树、B树、散列表

  2)无序查找和有序查找。
    无序查找:被查找数列有序无序均可;
    有序查找:被查找数列必须为有序数列。

  (3)平均查找长度(Average Search Length,ASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。

  (4)关键字(Key):是数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素。若关键字可以唯一地标识一个记录,则称此关键字为主关键字(Primary Key);若关键字对应多个记录,则称此关键字为次关键字(Secondary Key)。

 

 

二、顺序查找算法:

  (1)定义:顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。

  (2)最基础的实现方法:

int Search_Seq(SSTable ST,keyType key)
{
    for(i = ST.length;i>=1;--i)
        if(ST.R[i].key == key)
            return i;
    return 0;
}

  (3)带哨兵的实现方法:

// 有哨兵顺序查找数组a中从a[1]到数组末尾的key,无哨兵每次循环都需要对i是否越界,即是否小于a.length做判断。
// 设置一个哨兵可以解决不需要每次让i与a.length作比较。
// 返回-1说明查找失败,注意: 只能从数组下标为1的位置开始查找
 int seqSearchWithGuard(int[] a, int key)
 {
    int i = a.length - 1;    // 设置循环从数组尾部开始
    a[0] = key;              // 设置a[0]为关键字值,称之为"哨兵"
    while (a[i] != key)
        i--;
    if(i>0)
        return i;
    else
        return -1;
}

  (4)时间复杂度估算:

  对于顺序查找算法来说,查找成功最好的情况就是在第一个位置就找到了,算法时间复杂度为O(1),最坏的情况是在最后一位才找到,需要n次比较,时间复杂度为O(n),需要n+1次比较,时间复杂度为O(n),由于关键字在任意一位置的概率是相等的,所以平均查找次数为(n+1)/2,所以最终时间复杂度还是O(n)。当n很大时,查找效率极为低下,对一些小型数据的查找时,这种查找方式是可以适用的。

  

 

 

三、二分查找算法:

  (1)定义:二分查找(Binary Search),又称为折半查找。它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。二分查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。

  (2)基础实现:

// 二分查找:数组a中所有数据中包含key的数组下标,如果没有,返回-1
int binarySearch(int[] a, int key)
{
    int low = 0;                          // 定义最低下标为记录首位
    int high = a.length-1;                // 定义最高下标为记录末位
    while(low <= high)
    {
        int mid = (low + high)/2;         // 折半得到中间记录的下标
        if(key < a[mid])                  // 若查找值比中值小
            high = mid - 1;               // 最高下标调整到中间下标小一位
        else if(key > a[mid])              // 若查找值比中指大
            low = mid + 1;                // 最低下标调整到中间下标大一位
        else
            return mid;                   // 若相等则说明中间记录的下标即为查找到的值
    }
    return -1;
}

  (3)时间复杂度估算:

  二分查找等于是把静态有序查找分成了两棵子树,即查找结果只需要找其中的一半数据记录即可,等于工作量少了一半,然后继续折半查找,效率当然非常高了。

  根据二叉树的性质4,即“具有n个结点的完全二叉树的深度为【Log2n】+1”,可以得到二分查找最坏情况下查找到关键字或查找失败的次数是【Log2n】+1,最好的情况当然是1次了,因此二分查找的时间复杂度为O(Logn),显然远远好于顺序查找的O(n)时间复杂度了。

  但是缺点是,对表的结构要求很高,只能用于顺序存储的有序表,并且查找之前还需要排序,而排序本身就是一个比较费时的运算,不适合用于动态变化的线性表。

 

 

 

四、二叉排序树算法:

  (1)定义:又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树:

      =》若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

      =》若它的右子树不空,则右子树上所有节点的值均大于它的根结点的值;

      =》它的左、右子树也分别为二叉排序树

      对二叉排序树进行中序遍历就可以得到一个有序的序列{35,37,47,51,58,62,73,88,93,99}

  (2)实现方法:

      =》若查找树为空,则查找失败;

      =》若查找树为非空,则:

        ①若给定值key等于根结点的关键字值,则查找成功,结束查找过程,否则转②

        ②若给定值key小于根结点的关键字值,则继续在根结点的左子树上进行,否则转③

        ③若给定值key大于根结点的关键字值,则继续在根结点的右子树上进行。

// 在二叉排序树p中查找key,若查找成功返回true,查找不成功返回false
// 返回false有两种情况:1.二叉排序树p为空 2.二叉排序树p不为空但是没有查找到key
bool searchBST(int key) {
    BiTreeNode current = root;
    while(current != null)
    {
        if(key == current.data)
            return true;
        else if(key < current.data)
            current = current.lchild;
        else
            current = current.rchild;
    }
    return false;
}

 

   (3)时间复杂度估算:

    理想的二叉树是比较平衡的,其深度与完全二叉树相同,均为【log2N】+1,那么查找的时间复杂度也就是O(logn),近似于二分查找。不平衡的极端情况就是斜树,查找时间复杂度为O(n),等同于顺序查找。如何让二叉树平衡就成为了需要考虑的问题。

  (4)二叉排序树的基本操作算法:

#include<stdio.h>  
#include<stdlib.h>

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 100 /* 存储空间初始分配量 */

typedef int Status;    /* Status是函数的类型,其值是函数结果状态代码,如OK等 */ 

/* 二叉树的二叉链表结点结构定义 */
typedef  struct BiTNode    /* 结点结构 */
{
    int data;    /* 结点数据 */
    struct BiTNode *lchild, *rchild;    /* 左右孩子指针 */
} BiTNode, *BiTree;


/* 递归查找二叉排序树T中是否存在key, */
/* 指针f指向T的双亲,其初始调用值为NULL */
/* 若查找成功,则指针p指向该数据元素结点,并返回TRUE */
/* 否则指针p指向查找路径上访问的最后一个结点并返回FALSE */
Status SearchBST(BiTree T, int key, BiTree f, BiTree *p) 
{  
    if (!T)    /*  查找不成功 */
    { 
        *p = f;  
        return FALSE; 
    }
    else if (key==T->data) /*  查找成功 */
    { 
        *p = T;  
        return TRUE; 
    } 
    else if (key<T->data) 
        return SearchBST(T->lchild, key, T, p);  /*  在左子树中继续查找 */
    else  
        return SearchBST(T->rchild, key, T, p);  /*  在右子树中继续查找 */
}


/*  当二叉排序树T中不存在关键字等于key的数据元素时, */
/*  插入key并返回TRUE,否则返回FALSE */
Status InsertBST(BiTree *T, int key) 
{  
    BiTree p,s;
    if (!SearchBST(*T, key, NULL, &p)) /* 查找不成功 */
    {
        s = (BiTree)malloc(sizeof(BiTNode));
        s->data = key;  
        s->lchild = s->rchild = NULL;  
        if (!p) 
            *T = s;            /*  插入s为新的根结点 */
        else if (key<p->data) 
            p->lchild = s;    /*  插入s为左孩子 */
        else 
            p->rchild = s;  /*  插入s为右孩子 */
        return TRUE;
    } 
    else 
        return FALSE;  /*  树中已有关键字相同的结点,不再插入 */
}

/* 从二叉排序树中删除结点p,并重接它的左或右子树。 */
Status Delete(BiTree *p)
{
    BiTree q,s;
    if((*p)->rchild==NULL) /* 右子树空则只需重接它的左子树(待删结点是叶子也走此分支) */
    {
        q=*p; *p=(*p)->lchild; free(q);
    }
    else if((*p)->lchild==NULL) /* 只需重接它的右子树 */
    {
        q=*p; *p=(*p)->rchild; free(q);
    }
    else /* 左右子树均不空 */
    {
        q=*p; s=(*p)->lchild;
        while(s->rchild) /* 转左,然后向右到尽头(找待删结点的前驱) */
        {
            q=s;
            s=s->rchild;
        }
        (*p)->data=s->data; /*  s指向被删结点的直接前驱(将被删结点前驱的值取代被删结点的值) */
        if(q!=*p)
            q->rchild=s->lchild; /*  重接q的右子树 */ 
        else
            q->lchild=s->lchild; /*  重接q的左子树 */
        free(s);
    }
    return TRUE;
}

/* 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点, */
/* 并返回TRUE;否则返回FALSE。 */
Status DeleteBST(BiTree *T,int key)
{ 
    if(!*T) /* 不存在关键字等于key的数据元素 */ 
        return FALSE;
    else
    {
        if (key==(*T)->data) /* 找到关键字等于key的数据元素 */ 
            return Delete(T);
        else if (key<(*T)->data)
            return DeleteBST(&(*T)->lchild,key);
        else
            return DeleteBST(&(*T)->rchild,key);
         
    }
}

int main(void)
{    
    int i;
    int a[10]={62,88,58,47,35,73,51,99,37,93};
    BiTree T=NULL;
    
    for(i=0;i<10;i++)
        InsertBST(&T, a[i]);
    DeleteBST(&T,93);
    DeleteBST(&T,47);
    return 0;
}

 

 

 

 

五、平衡二叉树算法:

  (1)定义:

    平衡二叉树(Self-Balancing Binary Search Tree或Height-Balanced Binary Search Tree),是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1。平衡二叉树是一种高度平衡的二叉排序树,即要么是一棵空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF(Balance Factor),那么平衡二叉树上所有结点的平衡因子只可能是-1、0和1。距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,称为最小不平衡树。

   (2)基础算法实现:

   (此块忽略,平衡调整办法有点多,完全读懂有时间了再完整整理一篇文章出来)

 

 

 

六、B树简介:

  (1)背景:

      前面所讨论的查找算法都是在内存中进行的,它们适用于较小的文件,而对于较大的、存放在外存储器上的文件就不合适了,对于此类大规模的文件,即使是采用了平衡二叉树,在查找效率上仍然较低。

      如果要操作的数据集非常大,大到内存已经没办法处理了,这种情况下,对数据的处理需要不断从硬盘等存储设备中调入或调出内存页面。一旦涉及到这样的外部设备,关于时间复杂度的计算就会发生变化,访问该集合元素的时间已经不仅仅是寻找该元素所需比较次数的函数,必须考虑对硬盘等外部存储设备的访问时间以及将会对该设备做出多少次的单独访问。

   (2)定义:

      之前的树都是一个结点可以有多个孩子,但是它自身只存储一个元素,二叉树限制更多,结点最多只能有两个孩子。一个结点只能存储一个元素,在元素非常多的时候,就使得要么树的度非常大(结点拥有子树的个数的最大值),要么树的高度非常大,甚至两者都必须足够大才行。这就使得内存存取外存次数非常多,这显然成了时间效率上的瓶颈,因此,需要打破每一个结点只存储一个元素的限制。

      多路查找树(muiti-way search tree),其每一个结点的孩子树可以多于两个,且每一个结点处可以存储多个元素。

   (3)升级版之B+树定义:

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

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

      

         下面是B 树和B+树的区别图:

 

 

 

 

七、散列表查找(Hash Table)★★★★重要★★★★

   (1)定义:

      散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定存在在f(key)的位置上。   把对应关系f称为散列函数,又称为哈希(Hash)函数,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash Table)。关键字对应的记录存储位置称为散列地址。

   (2)查找步骤:

      散列技术既是一种存储方法,也是一种查找方法。散列过程的步骤分为两步: 在存储时,通过散列函数计算记录的散列地址,并按次散列地址存储该记录。 当查找记录时,通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。

   (3)适合场景:

      散列技术与线性表、树、图结构不同的是,散列技术的记录之间不存在什么逻辑关系,它只与关键字有关联。因此,散列主要是面向查找的存储结构。   散列技术最适合的求解问题是查找与给定值相等的记录。不适合同样的关键字对应很多记录或者范围查找。   对于两个不同的关键字key1≠key2,但是却有f(key1)=f(key2),这种现象称为冲突,并把key1和key2称为这个散列函数的同义词。

  (4)散列函数的构造方法:

      =》直接定址法 :f(key) = a X key + b(a,b为常数)

      =》数字分析法

      =》平方取中法

      =》折叠法

      =》除留余数法(最常用的散列函数):对于散列表长为m的散列函数公式为:f(key) = key mod p (p ≤m)。

      =》随机数法

  (5)处理散列冲突的方法:

  设计得再好的散列函数也不可能完全避免冲突。

  1.开放定址法:fi(key) = (f(key) + di) MOD m(di = 1,2,3...,m-1)

  开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

  假设关键字集合为{12,67,56,16,25,37,22,29,15,47,48,34},表长为12,f(key) = key MOD 12

  则f(12) = 0,f(67) = 7,f(56) = 8,f(16) = 4,f(25) = 1,而f(37) = 1,则f(37) = (f(37) + 1) MOD 12 = 2,继续f(22) = 10,f(29) = 5,f(15) = 3,f(47) = 11

  而f(48) = 0,冲突,f(48) = (f(48) + 1) MOD 12 = 1,也冲突,f(48) = (f(48) + 1) MOD 12 = 2,还是冲突,一直到f(48) = (f(48) + 6) MOD 12 = 6时才不冲突。

  把这种解决冲突的开放定址法称为线性探测法

  例如48和37这种本来都不是同义词却需要争夺一个地址的情况,称为堆积。堆积使得需要不断处理冲突,无论是存入还是查找效率都会大大降低。

  

  当key=34时,f(key)=10,但是22后面没有空位置了,反而它的前面有一个空位置,尽管可以不断地求余数后得到结果,但效率很差。可以改进di=1²,-1²,2²,-2²,...,q²,-q²(q≤m/2),这样就等于是可以双向寻找到可能的空位置。

  增加平方运算的目的是为了不让关键字都聚集在某一块区域,称这种方法为二次探测法fi(key) = (f(key) + di) MOD m (di=1²,-1²,2²,-2²,...,q²,-q²(q≤m/2))

  还可以对于位移量di采用随机函数计算得到,称之为随机探测法。即设置随机种子相同,每次调用随机函数可以生成不会重复的数列,在查找时,用同样的随机种子,它每次得到的数列是相同的,相同的di可以得到相同的散列地址。fi(key) = (f(key) + di) MOD m(di是一个随机数列)

  2.再散列函数法

  再散列函数法就是事先准备多个散列函数fi(key) = RHi(key) (i=1,2,...,k),每当发生散列地址冲突时,就换一个散列函数计算,这种方法能够使得关键字不产生聚集,也相应增加了计算的时间。  

  3.链地址法

  将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针,无论有多少个冲突,都只是在当前位置给单链表增加结点而已。

  例如:0下标→48→12,1下标→37→25...

  链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,也带来了查找时需要遍历单链表的性能损耗。

  4.公共溢出区法

  公共溢出区法就是将所有与之间的关键字位置有冲突的关键字{37,48,34}存入一个公共的溢出区表中。

  在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行对比,如果相等,则查找成功;如果不相等,则到溢出表中进行顺序查找。

  如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。

  (6)例题展示:Hashing

The task of this problem is simple: insert a sequence of distinct positive integers into a hash table, and output the positions of the input numbers. The hash function is defined to be H ( k e y ) = k e y % T S i z e whereT S i z e is the maximum size of the hash table. Quadratic probing (with positive increments only) is used to solve the collisions.

Note that the table size is better to be prime. If the maximum size given by the user is not prime, you must re-define the table size to be the smallest prime number which is larger than the size given by the user.

Input Specification:
Each input file contains one test case. For each case, the first line contains two positive numbers: M S i z e (≤ 1
  04​​) andN (≤ M S i z e ) which are the user-defined table size and the number of input numbers, respectively. ThenN distinct positive integers are given in the next line. All the numbers in a line are separated by a space.

Output Specification:
For each test case, print the corresponding positions (index starts from 0) of the input numbers in one line. All the numbers in a line are separated by a space, and there must be no extra space at the end of the line. In case it is impossible to insert the number, print "-" instead.

Sample Input:
4 4
10 6 4 15
Sample Output:
0 1 4 -
题干

 

    这道题目只需要用一个数组就可以解决,开始对数组每个元素先初始化为0,然后通过散列映射到数组中去,如果该映射的下标的元素值为0,则把该下标置为我们输入的值,否则就遍历正向的二次探测。这里大家可能会对什么时候才能结束探测循环表示疑问。

    对1来说,他不是素数(质数定义为在大于1的自然数中,除了1和它本身以外不再有其他因数),所以对他而言最小素数为2。

    首先需要构造获取素数的函数:

int GetPrime(int x)
{
    if(x == 1)    return 2;
    int p,i;
    if(x % 2 == 1) p = x;
    else p = x + 1;
    while(1)
    {
        for(i = sqrt(p); i >= 2; i--)
            if (p % i == 0)
                break;
        if(i == 1)
            break;
        else
            p += 2;
    }
    return p;
}

    之后就可以开始敲哈希了

    for(int i = 0; i < N; i++)
    {
        if (i != 0) printf(" ");
        scanf("%d", &x);
        pos = x % size;
        tempPos = pos;
        if(A[tempPos] == 0)
        {
            A[tempPos] = x;
            printf("%d", pos);
        }
        else
        {
            int cnt, flag = 0;
            for(cnt = 1; cnt < size; cnt++)
            {
                pos = (tempPos + cnt*cnt) % size;
                if(A[pos] == 0)
                {
                    flag = 1;
                    A[pos] = x;
                    printf("%d", pos);
                    break;
                }
            }
            if(flag == 0) printf("-");
        }
    }

    完整AC答案展示如下:

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#define MAX 99999999
int size,N,x,pos,tempPos,A[MAX];
int GetPrime(int x)
{
    if(x == 1)    return 2;
    int p,i;
    if(x % 2 == 1) p = x;
    else p = x + 1;
    while(1)
    {
        for(i = sqrt(p); i >= 2; i--)
            if (p % i == 0)
                break;
        if(i == 1)
            break;
        else
            p += 2;
    }
    return p;
}
int main()
{
    scanf("%d %d", &size, &N);
    size = GetPrime(size);

    for(int i = 0; i < size; i++)//初始化 
        A[i] = 0;
        
    for(int i = 0; i < N; i++)
    {
        if (i != 0) printf(" ");
        scanf("%d", &x);
        pos = x % size;
        tempPos = pos;
        if(A[tempPos] == 0)
        {
            A[tempPos] = x;
            printf("%d", pos);
        }
        else
        {
            int cnt, flag = 0;
            for(cnt = 1; cnt < size; cnt++)
            {
                pos = (tempPos + cnt*cnt) % size;
                if(A[pos] == 0)
                {
                    flag = 1;
                    A[pos] = x;
                    printf("%d", pos);
                    break;
                }
            }
            if(flag == 0) printf("-");
        }
    }
    return 0;
}

 

 

 

注:如果有更好的解法,真心希望您能够评论留言贴上您的代码呢~互相帮助互相鼓励才能成长鸭~ 

posted @ 2019-05-29 00:58  HiCoding、嗨威  阅读(700)  评论(0编辑  收藏  举报