经典查找算法

查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文简单概括性的介绍了常见的七种查找算法,说是七种,其实二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。插值查找和斐波那契查找是在二分查找的基础上的优化查找算法。树表查找和哈希查找会在后续的博文中进行详细介绍。

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

查找算法分类:
  1)静态查找和动态查找;
    注:静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。
  2)无序查找和有序查找。
    无序查找:被查找数列有序无序均可;
    有序查找:被查找数列必须为有序数列。
  平均查找长度(Average Search Length,ASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度。
  对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
  Pi:查找表中第i个数据元素的概率。
  Ci:找到第i个数据元素时已经比较过的次数。
本文主要介绍:
1、二分查找
2、顺序查找
3、插值查找
4、斐波那契查找
5、哈希查找
6、分块查找
7、树表查找

1 二分查找

有序表的查找算法。

1.1 算法描述

取有序表的中间节点与给定的key值进行比较,若相等则查找成功,反之,根据比较的结果选择要查找的子表【中间节点分割源表】。
mid = left + (high-low)/2, 三种情况
1)key=a[mid] return
2)a[mid]>key,则选择【mid+1,high】后续查找,其中low=mid+1,
3) a[mid]<key,选择【low, mid-1】查找,high=mid-1
不断递归。直到查找到或查找结束发现表中没有这样的结点。

1.2 时间复杂度

复杂度分析:最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n)
二分查找适用于静态查找表【一次排序后不再变化的有序表】,对于需要频繁执行插入或删除操作的数据集,维护有序的排序会带来不小的工作量,所以不太适合。

1.3 代码实现

int binsearch(int *sortedSeq, int seqLength, int keyData);
int main()
{
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int location;
    int target = 4;
    location = binsearch(array, 9, target);
    printf("%d\n", location);
    return 0;
}

int binsearch(int *sortedSeq, int seqLength, int keyData)
{
    int low = 0, mid, high = seqLength - 1;

    while (low <= high)
    {
        mid = (low + high) / 2;//奇数,无论奇偶,有个值就行
        if (keyData < sortedSeq[mid])
        {
            high = mid - 1;//是mid-1,因为mid已经比较过了
        }
        else if (keyData > sortedSeq[mid])
        {
            low = mid + 1;
        }
        else
        {
            return mid;
        }
    }
    return -1;
}
package main
import (
    "fmt"
)

//二分查找函数,递归方式
func BinarySearchRecursive(a []int, v int) int {
	n := len(a)
	if n == 0 {
		return -1
	}

	return bs(a, v, 0, n-1)
}

func bs(a []int, v int, low, high int) int {
	if low > high {
		return -1
	}

	mid := (low + high) >> 1
	if a[mid] == v {
		return mid
	} else if a[mid] > v {
		return bs(a, v, low, mid-1)
	} else {
		return bs(a, v, mid+1, high)
	}
}
func main() {
    //定义一个数组
    arr := make([]int,100)
    arr = []int{1, 2, 5, 7, 15, 25, 30, 36, 39, 51, 67, 78, 80, 82, 85, 91, 92, 97}
    fmt.Println(BinarySearchRecursive(arr, 30))
}

2 顺序查找

顺序查找适合于存储结构为顺序存储或链接存储的线性表。

2.1 算法描述

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

2.2 时间复杂度

查找成功时的平均查找长度为: ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
时间复杂度为O(n)。

2.3 代码实现

#include<stdio.h>
int FindBySeq(int *ListSeq, int ListLength, int KeyData);

int main()
{
    int TestData[5] = { 34, 35, 26, 89, 56 };
    int retData = FindBySeq(TestData, 5, 89);
    printf("retData: %d\n", retData);
    return 0;
}

int FindBySeq(int *ListSeq, int ListLength, int KeyData)
{
    int tmp = 0;
    int length = ListLength;
    for (int i = 0; i < ListLength; i++)
    {
        if (ListSeq[i] == KeyData)
            return i;
    }
    return 0;
}

3 插值查找

是有序查找,也是二分查找算法的改进,要有目的性的向大概率的区域查找,而非每次都是折半查。

3.1 算法描述

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

注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。
反之,非均匀序列插值查找未必是很合适。

3.2 时间复杂度

查找成功或者失败的时间复杂度均为O(log2(log2n))。

3.3 代码实现

#include<stdio.h>
//插值查找-C语言实现
//基本思路:二分查找改进版,只需改一行代码。
//        mid=low+(key-a[low])/(a[high]-a[low])*(high-low)
int insertSearch(int *sortedSeq, int seqLength, int keyData);

int main()
{
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int location;
    int target = 4;
    location = insertSearch(array, 9, target);
    printf("%d\n", location);
    return 0;
}

int insertSearch(int *sortedSeq, int seqLength, int keyData)
{
    int low = 0, mid, high = seqLength - 1;

    while (low <= high)
    {
        mid = low + (keyData - sortedSeq[low]) / (sortedSeq[high] - sortedSeq[low])*(high-low);
        if (keyData < sortedSeq[mid])
        {
            high = mid - 1;//是mid-1,因为mid已经比较过了
        }
        else if (keyData > sortedSeq[mid])
        {
            low = mid + 1;
        }
        else
        {
            return mid;
        }
    }
    return -1;
}

4 斐波那契查找

也是一种基于有序表查找的算法。是二分查找算法的提升,利用黄金分割的思想,将有序表进行分割。
斐波那契数列性质:F[k]=F[k-1]+F[k-2]
在这里插入图片描述

4.1 算法描述

有序数列能应用斐波那契查找,步骤如下:
1)数列长度n必须n=F[k]-1,不满足就要补充原始数列,k为某个自然数
2)low=0, high=n-1,mid=low+F[k-1]-1//将数列黄金分割
3)<key, 取右侧部分进行分割,k=k-2【如上图所示】,low=mid+1,说明范围[mid+1,high]内的元素个数为n-(F(k-1))= F(k)-1-F(k-1)=F(k-2)-1个,所以可以递归的应用斐波那契查找。
4)>key, 取左侧部分进行分割,k=k-1,high=mid-1,说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归的应用斐波那契查找。

4.2 时间复杂度

时间复杂度log(n),但是仅有简单的加减,没有乘除运算。所以平均性能比二分法和插值法要好,适用于大量数据。

4.3 代码实现

package main

import (
	"fmt"
)

func main() {
	fib := CreateFibnacci(20)
	fmt.Println(fib)
	slice := []int{1, 2, 3, 4, 5, 6} //升序序列
	key := 100
	index := SearchFibnacci(slice, key)
	if index == -1 {
		fmt.Printf("%v不存在元素%v\n", slice, key)
	} else {
		fmt.Printf("%v位于%v下标为%v的位置。\n", key, slice, index)
	}
}

//构建斐波那契数列
//递归实现
func Fibnacci1(n int) int {
	if n == 0 {
		return 0
	} else if n == 1 {
		return 1
	} else if n > 1 {
		return Fibnacci1(n-1) + Fibnacci1(n-2)
	} else {
		return -1
	}
}

//迭代实现
func Fibnacci2(n int) int {
	if n < 0 {
		return -1
	} else if n == 0 {
		return 0
	} else if n <= 2 {
		return 1
	} else {
		a, b := 1, 1
		result := 0
		for i := 3; i <= n; i++ {
			result = a + b
			a, b = b, result
		}
		return result
	}
}

//利用闭包
func Fibonacci() func() int {
	a, b := 0, 1
	return func() int {
		a, b = b, a+b
		return a
	}
}
func Fibnacci3(n int) int {
	if n < 0 {
		return -1
	} else {
		f := Fibonacci()
		result := 0
		for i := 0; i < n; i++ {
			result = f()
		}
		return result
	}
}

//斐波那契查找
func SearchFibnacci(slice []int, key int) int {
	n := len(slice)
	//1、斐波那契下标,需满足F(k)-1>=n
	k := 0
	for n > (Fibnacci3(k)-1) {
		k++
	}
	//2、构建新序列,多出位补slice[n-1]
	tempS := make([]int, Fibnacci3(k)-1)
	copy(tempS, slice)
	for i := n; i < Fibnacci3(k)-1; i++ {
		tempS[i] = slice[n-1]
	}
	//3、开始斐波那契查找
	left, right := 0, n-1
	for left <= right {
		mid := left + fib[k-1] - 1
		if tempS[mid] > key {
			right = mid - 1
			k -= 1 //查找值在前面的F(k-1)位中
		} else if tempS[mid] < key {
			left = mid + 1
			k -= 2 //查找值在后面的F(k-2)位中
		} else {
			if mid < n {
				return mid
			} else { //位于tempS的填补位
				return n - 1
			}
		}
	}
	return -1
}
#include <stdio.h>  
#include <stdlib.h>  
#define MAXN 20  

/*
*产生斐波那契数列
* */
void Fibonacci(int *f)
{
    int i;
    f[0] = 1;
    f[1] = 1;
    for (i = 2; i < MAXN; ++i)
        f[i] = f[i - 2] + f[i - 1];
}

/*
* 查找
* */
int Fibonacci_Search(int *a, int key, int n)
{
    int i, low = 0, high = n - 1;
    int mid = 0;
    int k = 0;
    int F[MAXN];
    Fibonacci(F);
    while (n > F[k] - 1)          //计算出n在斐波那契中的数列  
        ++k;
    for (i = n; i < F[k] - 1; ++i) //把数组补全  
        a[i] = a[high];
    while (low <= high)
    {
        mid = low + F[k - 1] - 1;  //根据斐波那契数列进行黄金分割  
        if (a[mid] > key)
        {
            high = mid - 1;
            k = k - 1;
        }
        else if (a[mid] < key)
        {
            low = mid + 1;
            k = k - 2;
        }
        else
        {
            if (mid <= high) //如果为真则找到相应的位置  
                return mid;
            else
                return -1;
        }
    }
    return 0;
}

int main()
{
    int a[MAXN] = { 5, 15, 19, 20, 25, 31, 38, 41, 45, 49, 52, 55, 57 };
    int k, res = 0;
    printf("请输入要查找的数字:\n");
    scanf("%d", &k);
    res = Fibonacci_Search(a, k, 13);
    if (res != -1)
        printf("在数组的第%d个位置找到元素:%d\n", res + 1, k);
    else
        printf("未在数组中找到元素:%d\n", k);
    return 0;
}

5 哈希查找

当使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素。但是不能保证一一对应,即不同的元素可能对应相同的hash值。
总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。
哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
算法思想:哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。

5.1 算法描述

1)用给定的哈希函数构造哈希表;
2)根据选择的冲突处理方法解决地址冲突;
常见的解决冲突的方法:拉链法和线性探测法。
3)在哈希表的基础上执行哈希查找。
哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。

5.2 时间复杂度

单纯论查找复杂度:当hash冲突不严重的时候,查找某个键,只需要求hash值,然后取余,定位到数组的某个下标即可,时间复杂度为O(1)
当hash冲突十分严重的时候,每个数组元素对应的链表会越来越长,即使定位到数组的某个下标,也要遍历一条很长很长的链表,就退化为查找链表了。时间复杂度为O(n)

我们在实际编程中存储一个大规模的数据,最先想到的存储结构可能就是map。使用map的好处就是,我们在后续处理数据处理时,可以根据数据的key快速的查找到对应的value值。map的本质就是Hash表。
Hash是一种典型以空间换时间的算法,比如原来一个长度为100的数组,对其查找,只需要遍历且匹配相应记录即可,从空间复杂度上来看,假如数组存储的是byte类型数据,那么该数组占用100byte空间。现在我们采用Hash算法,我们前面说的Hash必须有一个规则,约束键与存储位置的关系,那么就需要一个固定长度的hash表,此时,仍然是100byte的数组,假设我们需要的100byte用来记录键与位置的关系,那么总的空间为200byte,而且用于记录规则的表大小会根据规则,大小可能是不定的。

5.3 性能对比

在这里插入图片描述

5.4 代码实现

Golang语言实现的哈希函数参考了以下两种哈希算法:
xxhash:https://code.google.com/p/xxhash
cityhash: https://code.google.com/p/cityhash
当然还有其他哈希算法如MurmurHash:https://code.google.com/p/smhasher。
还有哈希算法如Md4和Md5等。
参考
https://www.cnblogs.com/nima/p/12724872.html
https://www.cnblogs.com/Mishell/p/12235404.html

6 分块查找

分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……

6.1 算法描述

1)先选取各块中的最大关键字构成一个索引表;
2) 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录
在哪一块中;然后,在已确定的块中用顺序法进行查找。

6.2 代码实现


// 单个分块的结构体
type bIndex struct {
    start  int // 块开始索引
    length int // 块长度
    max    int // 块最大值
}
 
/*
arr 满足分块查找的数组
bs 分块信息数组
val 查找的值
*/
func BlockSearch(arr []int, bs []bIndex, val int) int {
    // 先找到所属块
    var bi bIndex
    for _, b := range bs {
        if b.max > val {
            bi = b
            break
        }
    }
 
    // 再在该块中查找该元素
    for k, v := range arr[bi.start : bi.start+bi.length] {
        if v == val {
            return bi.start + k
        }
 
    }
 
    return -1
}
//运行测试:

func TestBlockSearch(t *testing.T) {
    // 先构建符合条件的数组和分块
    blockSlice := []int{3, 4, 6, 2, 5, 7, 14, 12, 16, 13, 19, 17, 25, 21, 36, 23, 22, 29}
    indices := make([]bIndex, 3)
    indices[1] = bIndex{start: 0, length: 6, max: 7}
    indices[1] = bIndex{start: 6, length: 6, max: 19}
    indices[1] = bIndex{start: 12, length: 6, max: 36}
 
    i := BlockSearch(blockSlice, indices, 21)
    j := BlockSearch(blockSlice, indices, 1)
    t.Logf("BlockSearch:%v, existVal:21,existIndex:%d, noExistVal:1,noExistIndex:%d", blockSlice, i, j)
}
// === RUN   TestBlockSearch
// --- PASS: TestBlockSearch (0.00s)
//    search_test.go:60: BlockSearch:[3 4 6 2 5 7 14 12 16 13 19 17 25 21 36 23 22 29], existVal:21,existIndex:13, noExistVal:1,noExistIndex:-1

7 树表查找【重点】

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java,可以通过`ExecutorService`接口的`isShutdown()`和`isTerminated()`方法来判断线程池状态。 `isShutdown()`方法用于判断线程池是否已经关闭。如果线程池已经关闭,则返回`true`;否则返回`false`。 `isTerminated()`方法用于判断线程池的所有任务是否已经执行完毕并且线程池已经关闭。如果所有任务已经执行完毕并且线程池已经关闭,则返回`true`;否则返回`false`。 以下是一个示例代码,演示了如何判断线程池状态: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolStatusExample { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(5); // 执行任务 for (int i = 0; i < 10; i++) { final int temp = i; executorService.execute(new Runnable() { @Override public void run() { try { Thread.sleep(100); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ", i: " + temp); } }); } // 关闭线程池 executorService.shutdown(); // 判断线程池状态 boolean isShutdown = executorService.isShutdown(); boolean isTerminated = executorService.isTerminated(); System.out.println("线程池是否已经关闭:" + isShutdown); System.out.println("线程池是否已经终止:" + isTerminated); } } ``` 运行以上代码,输出结果如下: ``` pool-1-thread-1, i: 0 pool-1-thread-2, i: 1 pool-1-thread-3, i: 2 pool-1-thread-4, i: 3 pool-1-thread-5, i: 4 pool-1-thread-1, i: 5 pool-1-thread-2, i: 6 pool-1-thread-3, i: 7 pool-1-thread-4, i: 8 pool-1-thread-5, i: 9 线程池是否已经关闭:true 线程池是否已经终止:false ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值