查找算法之二分查找(对半查找)

  当有序表采用顺序存储时,可以采用二分查找的方式查找指定关键字的元素。
  二分查找的基本思想是选择表中某一位置 i i i的元素 A i A_i Ai,设该元素的关键字为 K i K_i Ki,将 K i K_i Ki与待查找关键字 k e y key key比较。

关键字 K 0 K_0 K0 K 1 K_1 K1 K 2 K_2 K2 K 3 K_3 K3 K 4 K_4 K4 …… K i − 1 K_{i-1} Ki1 K i K_i Ki k i + 1 k_{i+1} ki+1 …… K n − 4 K_{n-4} Kn4 K n − 3 K_{n-3} Kn3 K n − 2 K_{n-2} Kn2 K n − 1 K_{n-1} Kn1 K n K_n Kn
元素值 A 0 A_0 A0 A 1 A_1 A1 A 2 A_2 A2 A 3 A_3 A3 A 4 A_4 A4 …… A i − 1 A_{i-1} Ai1 A i A_i Ai A i + 1 A_{i+1} Ai+1 …… A n − 4 A_{n-4} An4 A n − 3 A_{n-3} An3 A n − 2 A_{n-2} An2 A n − 1 A_{n-1} An1 A n A_n An

  比较的结果当然只有三种可能性:

  • k e y key key < K i K_i Ki:当带查找的关键字存在于有序表中,且该有序表是采用升序方式存储的,那么待查找关键字 k e y key key对应的元素肯定在子表( A 1 A_1 A1, A 2 A_2 A2,……, A i − 1 A_{i-1} Ai1)中。那么只需在子表中继续按这一规则进行查找便可完成查找。
  • k e y key key = K i K_i Ki:若待查找的关键字等于 K i K_i Ki时,则表示查找成功。
  • k e y key key > K i K_i Ki:当待查关键字大于当前关键字时,则表示待查找关键字对应的元素如果存储在表中,则一定在子表( A i + 1 A_{i+1} Ai+1, A i + 2 A_{i+2} Ai+2,……, A n A_n An)中,按这一规则继续查找该子表。
      那么这个 i i i如何确定呢,根据不同的规则来确定这个 i i i,可以得到不同的二分搜索方法,如对半搜索,斐波那契搜索和插值搜索等。

对半查找

  设查找子表中第一个元素的序号定为 l o w low low,最后一个元素的序号定为 h i g h high high,令 i = ( l o w + h i g h ) / 2 i=(low+high)/2 i=(low+high)/2,这种二分查找称为对半查找。对半查找算法将表划分为几乎相等的两个子表。
  从二分查找的思想中可以看出这种查找方式是一种递归查找,根据这种查找算法思想,可以很容易写出对半查找的递归算法:

typedef struct
{
	int key;
	int value;
}s_eletype;

typedef struct
{
	int size;
	s_eletype* element;
}s_list;


int _search(s_list lst, int k, int low, int high)
{
	int mid;
	if (low <= high)
	{
		mid = (low + high) / 2;
		if (k < lst.element[mid].key) return _search(lst, k, low, mid - 1);
		else if (k > lst.element[mid].key) return _search(lst, k, mid + 1, high);
		else return mid;
	}
	return -1;
}

int search(s_list lst, int k, s_eletype* x)
{
	int i = _search(lst, k, 0, lst.size - 1);
	if (i != -1)
	{
		*x = lst.element[i];
		return true;
	}
	else return false;

}

  递归算法效率往往比较低,在递归中过程中,需要不断调用递归函数本身,对栈开销比较大,调用过程中压栈和返回同样也需要花费大量时间。因此递归函数在能够转换成迭代算法时,一般采用迭代的方式进行运算。
  将_search函数以迭代的方式实现:

int _search(s_list lst, int k, int low, int high)
{
	int mid;
	while (low <= high)
	{
		mid = (low + high) / 2;
		if (k < lst.element[mid].key) high = mid - 1;
		else if (k > lst.element[mid].key) low = mid + 1;
		else return mid;
	}
	return -1;
}

  对半查找只适用于顺序存储的有序表,在运行过程中需要不断的计算子表的 m i d mid mid序号,这对于链式存储来说无法实现。

二叉判定树

  如果将有序数据构建成一颗平衡因子只有0-1的的二叉搜索平衡树,那么对半查找过程会变成什么样呢?以下列数据为例:

i 0 1 2 3 4 5 6 7 8 9
key 21 30 36 41 52 54 66 72 83 97

  如果要查找 k e y key key66的元素,对半查找的过程如下:

序号 0 1 2 3 4 5 6 7 8 9
第一次 21 30 36 41 52 54 66 72 83 97
第二次 21 30 36 41 52 54 66 72 83 97
第三次 21 30 36 41 52 54 66 72 83 97
第四次 21 30 36 41 52 54 66 72 83 97

  蓝色和绿色关键字表示该次查找的子表范围,绿色表示该次查找时与待查找关键字比较的关键字。
  这是查找成功的情况,那么查找失败的情况呢?例如要查找 k e y key key67的元素,很显然过程跟上面的过程一样,只是在第最后一次查找的时候仍然没有找到匹配的关键字,程序会继续执行一次,如果是迭代算法,那么low = mid+1,此时条件已经不满足while,程序返回-1
  将这些数据建成一颗平衡因子只有0-1的的二叉搜索平衡树,那颗树会长什么样?
查找算法之二分查找(对半查找)_第1张图片
  在这颗二叉搜索树上进行查找 k e y key key值为66的元素过程是怎么样的?
查找算法之二分查找(对半查找)_第2张图片
  这颗二叉搜索树的查找路径不就是对半查找的查找过程吗?那对半查找的性能分析就可以在这颗树的基础上进行了。
  一颗有n个节点的二叉判定树的高度为[lbn]+1(本篇中:[lbn]表示向下取整,{lbn}表示向上取整)。所以对半查找在查找成功的情况下,关键字值之间的比较次数不超过[lbn]+1;对于不成功的查找,需要进行[lbn][lbn]+1次比较。
  为了讨论简单,假定表的长度为 2 k − 1 2^k-1 2k1,即构成的二叉判定树为一颗满二叉树,二叉树的高度为 k k k=lb(n+1)根据二叉树的性质,在每个元素的查找概率相等时,可以求得查找成功是的平均搜索长度为
A S L s    =    1 n ∑ i = 1 k i ∗ 2 i − 1 = 1 n ∑ i = 1 k i ∗ ( 2 i − 2 i − 1 ) = l b ( n + 1 ) + l b ( n + 1 ) n − 1 ⩽ l b ( n + 1 )                                                        ( 当 n 较 大 时 ) \begin{array}{rcl}ASL_s\;&=&\;\frac1n\sum_{i=1}^ki\ast2^{i-1}=\frac1n\sum_{i=1}^ki\ast(2^i-2^{i-1})\\&\\&=&lb(n+1)+\frac{lb(n+1)}n-1\\&\\&\leqslant&lb(n+1)\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;(当n\mathrm{较大时})\end{array} ASLs==n1i=1ki2i1=n1i=1ki(2i2i1)lb(n+1)+nlb(n+1)1lb(n+1)(n)
  因此,在有序表中成功查找一个元素,对半查找的平均搜索长度为 O ( l b n ) O(lbn) O(lbn)。但是需要注意的是,这棵树在查找过程中并没有实际建立,这只是为了便于分析而建的分析模型。

本篇完

你可能感兴趣的:(数据结构)