算法就这么回事(一)七大查找算法汇总

2 篇文章 0 订阅

前言

查找(search)是在一个数据结合中查找满足给定条件的记录。对于查找问题来说,没有一种算法对于任何情况下都是合适的。有的查找速度比其他算法快,但是需要较多的存储空间(例如 Hash 查找);有的算法查找速度非常快,但仅用于有序数组(例如折半查找)。在实际应用中,如何在特大型规模的数据集合上进行高效查找具有非常重要的意义。

查找表(Search Table)是由同一类型的数据元素(或记录)构成的集合。对查找表经常进行的操作有:

  1. 查询某个数据元素是否在查找表中
  2. 检索某个数据元素的属性
  3. 向查找表中添加一个数据元素
  4. 从查找表中删除一个数据元素

若只涉及前两种操作,称这类查找表为静态查找表;若还涉及后两种操作,称这类查找表为动态查找表

本文介绍了常见的七种查找算法,有的应用在静态查找表中,有的应用在动态查找表中,均给出了 Java 的代码实现(为了方便说明,后面将以整型数据为例进行讲解,其他类型的数据查找算法与此类似),并对这些算法进行了简单地分析,感兴趣的小伙伴可以看看(有时间的话我会给这些算法的实现过程添加动图,便于大家的理解和记忆)。

顺序查找

基本思想

顺序查找也称为线形查找,属于无序查找算法。从数据结构线性表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于给定值的结点,表示查找失败。

代码实现

public int orderSearch(int[] array, int des) {
    int i = 0;
    for(; i < array.length; i++) {
        if(array[i] == des) {	// 逐个比较
            return i;
        }
    }
    return -1;
}

算法分析

  • 时间复杂度: O ( n ) O(n) O(n) ,顺序遍历的复杂度。

  • 空间复杂度: O ( 1 ) O(1) O(1) ,仅使用常数空间保存一些变量。

二分查找(折半查找)

基本思想

假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

注意,这里的前提条件是表中的元素是有序的,才能够使用二分查找算法。

代码实现

public int binarySearch(int[] array, int des) {
    int low = 0, high = array.length - 1;
    while(low <= high) {
        int mid = (low + high) / 2;
        if(des == array[mid]) {
            return mid;
        } else if(des > array[mid]) {	// 中值比目标值小
            low = mid + 1;
        } else {	// 中值比目标值大
            high = mid - 1;
        }
    }
    return -1;
}

算法分析

  • 时间复杂度: O ( l o g 2 n ) O(log_2n) O(log2n) ,每次查找缩小一半的查找范围。

  • 空间复杂度: O ( 1 ) O(1) O(1) ,仅使用常数空间保存一些变量。

插值查找

基本思想

插值查找,有序表的一种查找方式。插值查找是根据查找关键字与查找表中最大最小记录关键字比较后的查找方法。插值查找基于二分查找,将查找点的选择改进为自适应选择,提高查找效率。

这里自适应选择是什么意思呢?举个栗子,如果现在有一个单词 abandon 你不认识,你想要在字典里查这个单词的意思,你会怎么做,你会先翻到书中间,然后一半一半地向前找吗?你肯定会会将书翻到比较靠前的位置,找到以 a 开头的位置,然后考察第二个字母,以此类推,直到查到这个单词。你将书翻到靠前的位置这个动作就是自适应,因为你知道 abandon 这个单词大概率在字典的前面(乱序字典不算)。

很显然,插值查找是二分查找的改进,同时又对二分查找做了进一步的限制,就是待查找的元素要分布均匀,根据待查找元素可能出现的位置的概率(期望)进行下一步的范围选择,这时插值查找的效率才会优于二分查找,具体的查找点选择方式如下:

二分查找中的 mid 索引:
m i d = l o w + h i g h 2 = l o w + 1 2 ( h i g h − l o w ) mid=\frac {low+high}{2}=low+\frac 12(high-low) mid=2low+high=low+21(highlow)
插值查找中的 mid 索引:
m i d = l o w + d e s − a r r a y [ l o w ] a r r a y [ h i g h ] − a r r a y [ l o w ] ( h i g h − l o w ) mid=low+\frac{des-array[low]}{array[high]-array[low]}(high-low) mid=low+array[high]array[low]desarray[low](highlow)

代码实现

public int interpolationSearch(int[] array, int des) {
    int low = 0, high = array.length - 1;
    while(low <= high) {
        int mid = low + ((des - array[low]) / (array[high] - array[low])) * (high - low);
        if(mid < 0 || mid > array.length - 1) break;	// 通过概率选择 mid 索引可能会出现越界
        if(des == array[mid]) {
            return mid;
        } else if(des > array[mid]) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    return -1;
}

算法分析

  • 时间复杂度: O ( l o g 2 ( l o g 2 n ) ) O(log_2(log_2n)) O(log2(log2n)) ,每次查找缩小一定比例的查找范围,这个时间复杂度的证明比较复杂,感兴趣的小伙伴可以查看这篇论文

在这里插入图片描述

  • 空间复杂度: O ( 1 ) O(1) O(1) ,仅使用常数空间保存一些变量。

斐波拉契查找

基本思想

斐波那契搜索就是在二分查找的基础上根据斐波那契数列进行分割的。在斐波那契数列找一个等于略大于查找表中元素个数的数F[n],将原查找表扩展为长度为F[n]-1,可以补充重复最后一个元素,直到满足F[n]-1个元素,完成后进行斐波那契分割,即F[n]-1个元素分割为前半部分F[n-1]-1个元素,后半部分F[n-2]-1个元素,和中间F[n-1]位置的元素,找出要查找的元素在那一部分并递归,直到找到。

在这里插入图片描述

斐波拉契数列(Fibonacci sequence),又称为黄金分割数列,数学表达式如下:
F ( 0 ) = 0 , F ( 1 ) = 1 , F ( n ) = F ( n − 1 ) + F ( n − 2 ) ( n ≥ 2 , n ∈ N ∗ ) F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*) F(0)=0F(1)=1,F(n)=F(n1)+F(n2)(n2nN)
它的数据项如下,从第三个数开始,后边每一个数都是前两个数的和:

0,1,1,2,3,5,8,13,21,34,55,89,...

黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为 1:0.618 或 1.618:1 。

称它为黄金分割数列的原因是随着斐波那契数列的递增,前后两个数的比值会越来越接近 0.618,利用这个特性,我们就可以将黄金比例运用到查找技术中,提高查找效率。

所以不同于前面两种查找算法,斐波拉契查找选择黄金分割点作为数组的分割点,这种算法的明显优点在于它只涉及加法和减法运算,而不用除法。

代码实现

public int fibonacciSearch(int[] array, int des) {
    // 构造斐波拉契数列
    int[] fibonacci = new int[array.length];
    fibonacci[0] = 0;
    fibonacci[1] = 1;
    for (int i = 2; i < array.length; i++) {
        fibonacci[i] = fibonacci[i - 1] + fibonacci[i - 2];
    }

    int low = 0, high = array.length - 1;
    // 找到略大于数组长度的斐波拉契数列的项
    int n = 0;
    while (high > fibonacci[n] - 1) {
        n++;
    }
    // 对原数组扩充
    int[] tmp = Arrays.copyOf(array, fibonacci[n] - 1);
    for (int i = array.length; i < fibonacci[n] - 1; i++) {
        tmp[i] = array[high];
    }

    while (low <= high) {
        int mid = low + fibonacci[n - 1] - 1;
        if(des == tmp[mid]) {
            return Math.min(mid, high);		// 这句代码的意思是,当找到目标值时,比较目标值的下标和数组长度,当 mid > high 时,被查到的元素是复制的最后一个元素,所以要返回最后一个元素的下标
        } else if(des > tmp[mid]) {
            low = mid + 1;
            n-=2;
        } else {
            high = mid - 1;
            n--;
        }
    }
    return -1;
}

算法分析

  • 时间复杂度: O ( l o g 2 n ) O(log_2n) O(log2n) ,每次查找缩小一定比例的查找范围,由于它只涉及加法和减法运算,而不用除法,斐波那契查找的运行时间理论上比折半查找小。

  • 空间复杂度: O ( n ) O(n) O(n) ,需要对原数组进行复制扩容操作,使用了线性大小的额外空间。

分块查找(索引顺序查找)

基本思想

分块查找又称索引顺序查找,是对顺序查找的一种改进方法。在此查找方法中,除了表本身外,还需要建立一个索引表。对表进行分块,分成几个子表,将子表中的索引保存至索引表,索引表按关键字有序,则分块有序,即前一个子表中所有元素均小于后一个子表中所有元素(大于同理)。

在这里插入图片描述

因此,分块查找分为两步,第一步通过二分查找或顺序查找确定待查找元素所在的块,第二步,在块中进行顺序查找。

代码实现

public int blockSearch(int[] array, int[][] indexTable, int des) {
    // 查找索引表,找到元素所在块,这里使用顺序查找
    int pre, next, i = 0, length = indexTable[0].length;
    for (; i < length; i++) {
        if(des <= indexTable[0][i]) {
            break;
        }
    }
    if(i == length) {
        return -1;  // 比最大元素还大
    }
    // 块索引区间
    pre = indexTable[1][i];
    next = i == length - 1 ? length : indexTable[1][i + 1];
    // 在指定块中顺序查找元素
    for (int j = pre; j < next; j++) {
        if(des == array[j]) {
            return j;
        }
    }
    return -1;
}

算法分析

  • 时间复杂度: O ( l o g 2 n ) O(log_2n) O(log2n)~ O ( n ) O(n) O(n) ,分块查找相比顺序查找有了很大改进,但是远不及二分查找,具体时间复杂度还取决于分块的方式,这里只给出了范围。

  • 空间复杂度: O ( 1 ) O(1) O(1)~ O ( n ) O(n) O(n) ,除了表本身外,还需要构建索引表,索引表所占空间大小同样取决于分块的方式,这里只给出了范围。

树表查找

二叉排序树

基本思想

二叉排序树(Binary Sort Tree)或是一棵空树,或是具有下列性质的二叉树:

  1. 若它的左子树不为空,则左子树上的所有结点的值均小于它的根结点的值;
  2. 若它的右子树不为空,则右子树上的所有结点的值均大于它的根结点的值;
  3. 它的左右子树也是二叉排序树。

在这里插入图片描述

二叉排序树又称二叉查找树,它的查找过程如下,当二叉排序树不为空时,首先将给定值和根结点进行比较,若相等,则查找成功;若小于根结点,则在左子树上继续查找;若大于根结点,则在右子树上继续查找。

代码实现
// 树结点定义
class TreeNode {
    int val;		// 值
    TreeNode left;	// 左孩子
    TreeNode right;	// 右孩子
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

// 递归实现
public TreeNode BSTSearch(TreeNode root, int des) {
    if(root == null || root.val == des) {
        return root;
    }
    if(root.val > des) {
        return BSTSearch(root.left, des);
    } else {
        return BSTSearch(root.right, des);
    }
}

// 迭代实现
public TreeNode BSTSearch2(TreeNode root, int des) {
    while (root != null && des != root.val)
        root = des < root.val ? root.left : root.right;
    return root;
}
算法分析
  • 时间复杂度: O ( H ) O(H) O(H) ,H是树高,平均时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n),最坏情况下,二叉排序树退化成单支树,成为链表,此时时间复杂度为 O ( n ) O(n) O(n)

  • 空间复杂度: O ( 1 ) O(1) O(1) ,仅使用常数空间保存一些变量。

下面的几种树的构造较为复杂,但查找思想都和二叉排序树大同小异,这里仅对这些树以及查找过程作一个简单的介绍。

平衡二叉排序树

平衡二叉树(Balanced Binary Tree)又称 AVL 树。它或是一棵空树,或是具有下列性质的二叉树:它的左右子树均是平衡二叉树,且左右子树的高度差的绝对值不超过1

如果构造二叉排序树的同时保证它是平衡二叉树,那么这样的树称为平衡二叉排序树(BBST),可以证明它的深度是和 O ( l o g 2 n ) O(log_2n) O(log2n) 一个数量级的,那么在进行查找时的时间复杂度就可以固定在 O ( l o g 2 n ) O(log_2n) O(log2n) ,而不会出现二叉排序树中的最坏的情况。

下面是一棵平衡二叉排序树:

在这里插入图片描述

平衡二叉排序树的查找过程和二叉排序树完全相同。

B-树

B-树是一种平衡的多路(查找路径不止两条)查找树,它在文件系统中很有用。
一棵 m 阶的B-树,或为空树,或为满足下列特性的 m 叉树:

  1. 树中每个结点至多有 m 棵子树;
  2. 若根结点不是叶子结点,则至少有两棵子树;
  3. 除根之外的所有非终端结点至少有 ⌈ m / 2 ⌉ \lceil{m/2}\rceil m/2 棵子树;
  4. 所有的非终端结点中包含下列信息数据

( n , A 0 , K 1 , A 1 , K 2 , A 2 , . . . , K n , A n ) (n,A_0,K_1,A_1,K_2,A2,...,K_n,A_n) (n,A0,K1,A1,K2,A2,...,Kn,An)

其中 K i K_i Ki 为关键字,且 K i < K i + 1 K_i<K_{i+1} Ki<Ki+1 A i A_i Ai 为指向子树根节点的指针,且 A i − 1 A_{i-1} Ai1 所指向的子树中的所有结点关键字均小于 K i K_i Ki A i A_i Ai 所指向的子树所有关键字均大于 K i K_i Ki

  1. 所有的叶子结点都出现在同一层次上,且不携带信息(可以看作 null)。

特别地,3阶B-树又称为2-3树,下面是一棵4阶的B-树:

在这里插入图片描述

B-树的查找和二叉排序树类似,首先从根结点开始,逐层往下顺指针查找结点,然后在结点的关键字中进行查找。

举个栗子,如果要查找关键字47,首先从根节点开始,因为47>35,所以关键字若存在必在 A 1 A_1 A1 所指的子树中,找到下一个结点,该节点有两个关键字,而43<47<78,所以关键字若存在必在 A 1 A_1 A1 所指的子树中,找到下一个节点,该节点有3个关键字,顺序查找找到了关键字47。

由此可见,B-树的查找主要涉及两个操作:在树中找结点;在结点中找关键字。

B+

B+树是应文件系统所需而出的一种B-树的变型树。一棵 m 阶的B+树和 m 阶的B-树的差异在于:

  1. 有 n 棵子树的结点中含有n个关键字;
  2. 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接
  3. 所有的非终端结点可以看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字。

下面是一棵3阶的B+树:

在这里插入图片描述

概括来说就是相比于B-树,B+树的非叶子结点不包含关键字的全部信息,仅保存最值充当索引,所有的数据信息保存在叶子结点中,而且叶子结点按关键字大小顺序链接。

因此,对于B+树,有两种查找算法,一种是从根结点开始进行随机查找;一种是从最小关键字开始,顺着链表顺序查找。随机查找基本和B-树相同,不同之处是,每次比较关键值相等时,并不会停止,而是会继续向下直到叶子结点。

红黑树

红黑树是每个结点都带有颜色属性的二叉查找树,颜色或红色或黑色,相对于二叉查找树,红黑树增加了以下额外要求:

  1. 结点是红色或黑色;

  2. 根结点是黑色;

  3. 所有叶子都是黑色;

  4. 每个红色结点的两个子结点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色结点);

  5. 从任一节结点其每个叶子的所有路径都包含相同数目的黑色结点。

通过对任何一条从根到叶子简单路径上的颜色来约束,红黑树保证最长路径不超过最短路径的两倍,因而近似于平衡。

下面是一棵红黑树:

在这里插入图片描述

红黑树的基本思想是用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表示2-3树。

哈希查找

基本思想

上面介绍的查找方法都是建立在”比较“的基础上进行的,通过有限次数的比较,不断缩小范围,直至查找完成。很显然,这种算法的效率依赖于查找过程中比较的次数。

那与没有一种方法可以不经过比较就得到待查找的记录呢?不难想到如果想要实现这种想法,就需要将关键字映射到一个唯一确定的位置,每次查找时只需要获取这个位置即可,问题就是如何确定这个映射关系。

这个映射关系称为哈希(Hash)函数或散列函数,按照这个函数映射出的表称为哈希表(Hash Table)或者散列表。

构造哈希函数的方法有很多,一个好的哈希函数因该尽可能地减少冲突,尽可能地将关键字均匀的映射到哈希表中。

常用的构造哈希函数地方法有:

  1. 直接地址法:取关键字或关键字的某个线性函数值为散列地址。
  2. 数字分析法:假设关键字是以 r 为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。
  3. 平方取中法:取关键字平方后的中间几位为哈希地址。
  4. 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。
  5. 除留取余法:取关键字被某个不大于散列表表长m的数p除后所得的余数为哈希地址。
  6. 随机数法:选则一个随机函数,取关键字的随机函数值为它的哈希地址。

常用的处理冲突的方法有:

  1. 开放地址法
  2. 再哈希法
  3. 链地址法
  4. 建立公共溢出区

哈希查找的操作步骤:

  1. 用给定的哈希函数构造哈希表;
  2. 根据选择的冲突处理方法解决地址冲突;
  3. 在哈希表的基础上执行哈希查找。

在哈希查找的过程中,只需先将要查找的数据映射为它的哈希值,然后查找具有这个哈希值的数据,这就大大减少了查找次数。如果构造哈希函数的参数经过精心设计,内存空间也足以存放哈希表,查找一个数据元素所需的比较次数基本上就接近于一次。

代码实现

public int hashSearch(int[] hashTable, int des) {
    // 计算哈希值
    int hashAddress = hash(hashTable, des);
    while (hashTable[hashAddress] != des) {
        // 这里采用开放地址线性探测法解决冲突
        hashAddress = (hashAddress + 1) % hashTable.length;
        // 等于初始值或者循环有回到了原点,则表示关键字不存在
        if (hashTable[hashAddress] == Integer.MIN_VALUE || hashAddress == hash(hashTable, des)) {
            return -1;
        }
    }
    // 返回关键字在哈希表中的下标
    return hashAddress;
}

public void insertHashTable(int[] hashTable, int num) {
    // 计算 hash 地址
    int hashAddress = hash(hashTable, num);
    // 哈希表初始填充 Integer.MIN_VALUE,如果不等于则发生了冲突
    while (hashTable[hashAddress] != Integer.MIN_VALUE) {
        // 这里采用开放地址线性探测法解决冲突
        hashAddress = (hashAddress + 1) % hashTable.length;
    }
    // 将数据插入哈希表
    hashTable[hashAddress] = num;
}

private int hash(int[] hashTable, int num) {
    // 除留取余法,余数这里选择的是哈希表长
    return num % hashTable.length;
}

算法分析

  • 时间复杂度: O ( 1 ) O(1) O(1) ,最差的情况下为 O ( n ) O(n) O(n)

  • 空间复杂度: O ( n ) O(n) O(n) ,需要额外空间保存哈希表。

性质汇总

查找算法(平均)时间复杂度空间复杂度条件
顺序查找 O ( n ) O(n) O(n) O ( 1 ) O(1) O(1)
二分查找 O ( l o g 2 n ) O(log_2n) O(log2n) O ( 1 ) O(1) O(1)有序
插值查找 O ( l o g 2 ( l o g 2 n ) ) O(log_2(log_2n)) O(log2(log2n)) O ( 1 ) O(1) O(1)有序
斐波拉契查找 O ( l o g 2 n ) O(log_2n) O(log2n) O ( n ) O(n) O(n)有序
分块查找 O ( l o g 2 n ) O(log_2n) O(log2n)~ O ( n ) O(n) O(n) O ( 1 ) O(1) O(1)~ O ( n ) O(n) O(n)块间有序
树表查找(BST) O ( l o g 2 n ) O(log_2n) O(log2n) O ( 1 ) O(1) O(1)
哈希查找 O ( 1 ) O(1) O(1) O ( n ) O(n) O(n)

>下一篇:排序

  • 33
    点赞
  • 274
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Python的查找算法包括二分查找算法和线性查找算法。 二分查找算法是一种高效的查找算法,它的前提条件是数据必须有序。该算法通过将当前列表不断分成两部分,然后跟踪最低和最高的两个索引,直到找到目标值为止。二分查找算法的Python代码如下: ```python def binary_search(list, item): first = 0 last = len(list) - 1 found = False while first <= last and not found: midpoint = (first + last) // 2 if list[midpoint == item: found = True else: if item < list[midpoint]: last = midpoint - 1 else: first = midpoint + 1 return found ``` 线性查找算法是一种简单直接的查找算法,它逐个匹配数据元素,直到找到目标值或遍历完整个列表。线性查找算法的Python代码如下: ```python def linear_search(list, item): index = 0 found = False while index < len(list) and not found: if list[index == item: found = True else: index += 1 return found ``` 这两种算法都可以用于在列表中查找特定的元素,但二分查找算法在有序数据上的查找效率更高。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [手把手教你用Python实现查找算法](https://blog.csdn.net/zw0Pi8G5C1x/article/details/121881880)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值