Go Book / 5 Data Structure & Algorithms / 17 基本查找算法:插值查找与斐波那契查找

17 基本查找算法:插值查找与斐波那契查找

一、插值查找

原理

在介绍插值查找之前,首先考虑一个新问题,为什么二分查找算法一定要是折半,而不是折四分之一或者折更多呢?

打个比方,在英文字典里面查“apple”,你下意识翻开字典是翻前面的书页还是后面的书页呢?如果再让你查“zoo”,你又怎么查? 很显然,这里你绝对不会是从中间开始查起,而是有一定目的的往前或往后翻。

同样的,比如要在取值范围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(log2(log2n))。

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

Go语言描述
func InsertionSearch(arr []int, v, start, end int) int {
	if start <= end {
		mid := (v - arr[start]) / (arr[end] - arr[start]) * (end - start)
		// fmt.Println("mid:",mid)
		if arr[mid] == v {
			return mid
		} else if arr[mid] > v {
			// 左边
			InsertionSearch(arr, v, start, mid-1)
		} else {
			// 右边
			InsertionSearch(arr, v, mid+1, end)
		}
	}
	return -1
}

二、斐波那契查找

原理

上面我们讲了插值查找,它是基于折半查找改进,然后除了插值方式的切割外,还有基于斐波那契黄金分割点切割方式的改进。 我们先来了解一下斐波那契数列的特性:

|————— F(K)-1 —————| |________|| |——- F(K-1)-1 —–|— F(K-2)-1 –| 斐波那契数列,又称黄金分割数列,之所以它又称为黄金分割数列,是因为它的前一项与后一项的比值随着数字数量的增多逐渐逼近黄金分割比值0.618。 所以斐波那契查找改变了二分查找中原有的中值 mid 的求解方式,其 mid 不再代表中值,而是代表了黄金分割点: mid = left + F_{block - 1} - 1

#####基本思想: 假设表中有 n 个元素,查找过程为取区间中间元素的下标 mid ,对 mid 的关键字与给定值的关键字比较: (1)如果与给定关键字相同,则查找成功,返回在表中的位置; (2)如果给定关键字大,向右查找并减小2个斐波那契区间; (3)如果给定关键字小,向左查找并减小1个斐波那契区间; (4)重复过程,直到找到关键字(成功)或区间为空集(失败)。

复杂度分析:

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

Go语言描述
func FibonacciSearch(arr []int, v int) int {
	fibs := getFibonacciArray(10)
	fmt.Println("fibonacciArray:", fibs)

	// 根据斐波那契数列找适合的区间
	k := 0
	l := len(arr)
	for l > fibs[k] {
		k++
	}

	// 2、构建新序列,多出位补slice[n-1]
	tmpArr := make([]int, fibs[k]-1)
	copy(tmpArr, arr)
	for i := l; i < len(tmpArr); i++ {
		tmpArr[i] = arr[l-1]
	}

	// 开始斐波那契查找
	left, right := 0, l-1
	for left <= right {
		// 找黄金分割点
		mid := left + fibs[k-1] - 1
		// fmt.Println("mid:", mid)
		// fmt.Println("tmpArr:", tmpArr)

		if tmpArr[mid] == v {
			if mid < l {
				return mid
			} else {
				// 位于tempS的填补位
				return l - 1
			}
		} else if tmpArr[mid] > v {
			// 左边
			right = mid - 1
			// 查找值在前面的fibs(k-1)区间中
			k -= 1
		} else {
			// 右边
			left = mid + 1
			// 查找值在后面的fibs(k-2)区间中
			k -= 2
		}
	}
	return -1
}

// 获取斐波那契数列
func getFibonacciArray(n int) []int {
	fArr := make([]int, n+1, n+1) // 数列第一位从下标1开始

	fArr[1] = 1
	fArr[2] = 1

	for i := 3; i <= n; i++ {
		fArr[i] = fArr[i-1] + fArr[i-2]
	}

	return fArr[1:]
}

小结

  • 折半查找:以一半元素来分割数组;
  • 插值查找:根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key;
  • 斐波那契查找:以斐波那契数列元素之间的黄金分割点来分割数组。