首页 二分查找

二分查找

举报
开通vip

二分查找二分查找 二分查找算法基本思想 二分查找算法的前置条件是,一个已经排序好的序列(在本篇文章中为了说明问题的方便,假设这个序列是升序排列的),这样在查找所要查找的元素时,首先与序列中间的元素进行比较,如果大于这个元素,就在当前序列的后半部分继续查找,如果小于这个元素,就在当前序列的前半部分继续查找,直到找到相同的元素,或者所查找的序列范围为空为止. 用伪代码来表示, 二分查找算法大致是这个样子的: left = 0, right = n -1 while (left t: right = mid -1; ...

二分查找
二分查找 二分查找算法基本思想 二分查找算法的前置条件是,一个已经排序好的序列(在本篇文章中为了说明问题的方便,假设这个序列是升序排列的),这样在查找所要查找的元素时,首先与序列中间的元素进行比较,如果大于这个元素,就在当前序列的后半部分继续查找,如果小于这个元素,就在当前序列的前半部分继续查找,直到找到相同的元素,或者所查找的序列范围为空为止. 用伪代码来表示, 二分查找算法大致是这个样子的: left = 0, right = n -1 while (left <= right) mid = (left + right) / 2 case x[mid] < t: left = mid + 1; x[mid] = t: p = mid; break; x[mid] > t: right = mid -1; return -1; 第一个正确的程序 根据前面给出的算法思想和伪代码, 我们给出第一个正确的程序,但是,它还有一些小的问题,后面会讲到 int search(int array[], int n, int v) { int left, right, middle; left = 0, right = n - 1; while (left <= right) { middle = (left + right) / 2; if (array[middle] > v) { right = middle; } else if (array[middle] < v) { left = middle; } else { return middle; } } return -1; } 下面,讲讲在编写二分查找算法时可能出现的一些问题. 边界错误造成的问题 二分查找算法的边界,一般来说分两种情况,一种是左闭右开区间,类似于[left, right),一种是左 闭右闭区间,类似于[left, right].需要注意的是, 循环体外的初始化条件,与循环体内的迭代步 骤, 都必须遵守一致的区间规则,也就是说,如果循环体初始化时,是以左闭右开区间为边界 的,那么循环体内部的迭代也应该如此.如果两者不一致,会造成程序的错误.比如下面就是错 误的二分查找算法: int search_bad(int array[], int n, int v) { int left, right, middle; left = 0, right = n; while (left < right) { middle = (left + right) / 2; if (array[middle] > v) { right = middle - 1; } else if (array[middle] < v) { left = middle + 1; } else { return middle; } } return -1; } 这个算法的错误在于, 在循环初始化的时候,初始化right=n,也就是采用的是左闭右开区间, 而当满足array[middle] > v的条件是, v如果存在的话应该在[left, middle)区间中,但是这里却 把right赋值为middle - 1了,这样,如果恰巧middle-1就是查找的元素,那么就会找不到这个 元素. 下面给出两个算法, 分别是正确的左闭右闭和左闭右开区间算法,可以与上面的进行比较: (下面这两个算法是正确的) int search2(int array[], int n, int v) { int left, right, middle; left = 0, right = n - 1; while (left <= right) { middle = (left + right) / 2; if (array[middle] > v) { right = middle - 1; } else if (array[middle] < v) { left = middle + 1; } else { return middle; } } return -1; } int search3(int array[], int n, int v) { int left, right, middle; left = 0, right = n; while (left < right) { middle = (left + right) / 2; if (array[middle] > v) { right = middle; } else if (array[middle] < v) { left = middle + 1; } else { return middle; } } return -1; } 死循环 上面的情况还只是把边界的其中一个写错, 也就是右边的边界值写错, 如果两者同时都写 错的话,可能会造成死循环,比如下面的这个程序: int search_bad2(int array[], int n, int v) { int left, right, middle; left = 0, right = n - 1; while (left <= right) { middle = (left + right) / 2; if (array[middle] > v) { right = middle; } else if (array[middle] < v) { left = middle; } else { return middle; } } return -1; } 这个程序采用的是左闭右闭的区间.但是,当array[middle] > v的时候,那么下一次查找的区间应该为[middle + 1, right], 而这里变成了[middle, right];当array[middle] < v的时候,那么下一次查找的区间应该为[left, middle - 1], 而这里变成了[left, middle].两个边界的选择都出现了问题, 因此,有可能出现某次查找时始终在这两个范围中轮换,造成了程序的死循环. 溢出 前面解决了边界选择时可能出现的问题, 下面来解决另一个问题,其实这个问题严格的说不属于算法问题,不过我注意到很多地方都没有提到,我觉得还是提一下比较好. 在循环体内,计算中间位置的时候,使用的是这个表达式: middle = (left + right) / 2; 假如,left与right之和超过了所在类型的表示范围的话,那么middle就不会得到正确的值. 所以,更稳妥的做法应该是这样的: middle = left + (right - left) / 2; 更完善的算法 前面我们说了,给出的第一个算法是一个"正确"的程序, 但是还有一些小的问题. 首先, 如果序列中有多个相同的元素时,查找的时候不见得每次都会返回第一个元素的位置, 比如考虑一种极端情况:序列中都只有一个相同的元素,那么去查找这个元素时,显然返回的是中间元素的位置. 其次, 前面给出的算法中,每次循环体中都有三次情况,两次比较,有没有办法减少比较的数量进一步的优化程序? <<编程珠玑>>中给出了解决这两个问题的算法,结合前面提到溢出问题我对middle的计算也做了修改: int search4(int array[], int n, int v) { int left, right, middle; left = -1, right = n; while (left + 1 != right)//这个循环维持的条件是left= n || array[right] != v) { right = -1; } return right; } 这个算法是所有这里给出的算法中最完善的一个,正确,精确且效率高. 但是这个算法的还是不能很好的理解 可以用下面的算法,可以找出满足条件的数 [cpp] view plaincopy 1. int Bi_Search(int a[],int n,int b)// 2. {//返回等于b的第一个 3. if(n==0) 4. return -1; 5. int low = 0; 6. int high = n-1; 7. int last = -1;//用last记录上一次满足条件的下标 8. while (low<=high) 9. { 10. int mid = low +(high-low)/2; 11. if (a[mid]==b) 12. { 13. last = mid; 14. high = mid -1; 15. } 16. else if(a[mid]>b) 17. high = mid -1; 18. else 19. low = mid +1; 20. } 21. 22. return last; 23. 24. } 25. int Bi_Search1(int a[],int n,int b)//大于b的第一个数 26. { 27. if(n<=0) 28. return -1; 29. int last = -1; 30. int low = 0; 31. int high = n-1; 32. while (low<=high) 33. { 34. int mid = low +(high - low)/2; 35. if(a[mid]>b) 36. { 37. last = mid; 38. high = mid -1; 39. } 40. else if (a[mid]<=b) 41. { 42. low =mid +1; 43. } 44. } 45. 46. return last; 47. } 查找(二):二分查找 一、二分查找(Binary Search) 二分查找又称折半查找,它是一种效率较高的查找方法。 二分查找要求:线性表是有序表,即表中结点按关键字有序,并且要用向量作为表的 存储结构。不妨设有序表是递增有序的。 二、二分查找的基本思想 二分查找的基本思想是:(设R[low..high]是当前的查找区间) (1)首先确定该区间的中点位置: (2)然后将待查的K值与R[mid].key比较:若相等,则查找成功并返回此位置,否则须确定新的查找区间,继续二分查找,具体方法如下: ?若R[mid].key>K,则由表的有序性可知R[mid..n].keys均大于K,因此若表中存在关键字等于K的结点,则该结点必定是在位置mid左边的子表R[1..mid-1]中,故新的查找区间是左子表R[1..mid-1]。 ?类似地,若R[mid].key R, int Key) { int low = 0, high = R.GetLength() - 1, mid;//置当前查找区间上、下界的初值 while (low <= high) { //当前查找区间R[low..high]非空 mid = low + ((high - low) / 2) ; if (R.Data[mid] == Key) return mid; //查找成功返回 if (R.Data[mid] > Key) high = mid - 1; //继续在R[low..mid-1]中查找 else low = mid + 1; //继续在R[mid+1..high]中查找 } return -1; //当low>high时表示查找区间为空,查找失败 } 四、二分查找判定树 二分查找过程可用二叉树来描述:把当前查找区间的中间位置上的结点作为根,左子表和右子表中的结点分别作为根的左子树和右子树。由此得到的二叉树,称为描述二分查找的判定树(Decision Tree)或比较树(Comparison Tree)。 注意: 判定树的形态只与表结点个数n相关,而与输入实例中R[1..n].keys的取值无关。 【例】具有11个结点的有序表可用下图所示的判定树来表示。 (1)二分查找判定树的组成 ?圆结点即树中的内部结点。树中圆结点内的数字表示该结点在有序表中的位置。 ?外部结点:圆结点中的所有空指针均用一个虚拟的方形结点来取代,即外部结点。 ?树中某结点i与其左(右)孩子连接的左(右)分支上的标记"<"、"("、">"、")"表示:当待查关键字KR[i].key)时,应走左(右)分支到达i的左(右)孩子,将该孩子的关键字进一步和K比较。若相等,则查找过程结束返回,否则继续将K与树中更下一层的结点比较。 (2)二分查找判定树的查找 二分查找就是将给定值K与二分查找判定树的根结点的关键字进行比较。若相等,成功。否则若小于根结点的关键字,到左子树中查找。若大于根结点的关键字,则到右子树中查找。 【例】对于有11个结点的表,若查找的结点是表中第6个结点,则只需进行一次比较;若查找的结点是表中第3或第9个结点,则需进行二次比较;找第1,4,7,10个结点需要比较三次;找到第2,5,8,11个结点需要比较四次。 由此可见,成功的二分查找过程恰好是走了一条从判定树的根到被查结点的路径,经历比较的关键字次数恰为该结点在树中的层数。若查找失败,则其比较过程是经历了一条从判定树根到某个外部结点的路径,所需的关键字比较次数是该路径上内部结点的总数。 【例】待查表的关键字序列为:(05,13,19,21,37,56,64,75,80,88,92),若要查找K=85的记录,所经过的内部结点为6、9、10,最后到达方形结点"9-10",其比较次数为3。 实际上方形结点中"i-i+1"的含意为被查找值K是介于R[i].key和R[i+1].key之间的,即R[i].key 5 void t_merge( T& v, size_t size, size_t pos ) 6 { 7 size_t fir = 0, sec = pos; 8 while ( fir < sec && sec < size ) 9 { 10 while ( fir < sec && v[fir] <= v[sec] ) fir++; 11 size_t maxMove = 0; 12 while ( sec < size && v[fir] > v[sec] ) maxMove++, sec++; 13 t_exchange( &v[fir], sec - fir, sec - fir - maxMove ); 14 fir += maxMove; 15 } 16 } 其中T是一个数组, size是数组尺寸, pos是归并划分的位置.也就是说[0,pos)和[pos, size)都分别是有序的.比如对序列1, 3, 5, 7, 2, 4, 6, 8进行归并操作, 此时size=8, pos = 4. 以<<算法导论>>中介绍的通过循环不变量的方法证明算法的正确性,在这个算法中, 循环不变量为[fir, sec)中的元素都是有序的: 1) 初始:此时fir = 0, sec = pos, 以前面介绍的函数参数的说明来看,满足循环不变量. 2) 迭代:来看看循环做了些什么操作.行10进行的操作为, 只要满足fir元素不大于sec元素, fir就一直递增;行12进行的操作是只要满足fir大于sec, sec就一直递增, 同时递增maxMove计数.因此, 进行完前面两个步骤之后, fir所指元素一定小于sec以及其后的所有元素.而位于sec之前的第二个子序列中的元素, 一定小于fir.因此, [sec-maxMove, sec)z中的元素小于所有[fir, sec - 1)的元素.通过调用t_exchange函数将位于[sec-maxMove, sec)中的元素"旋转"到fir之前. 也就是说, 这个过程在第二个已经排序好的子序列中寻找在它之内的小于目前第一个已经排序好的子序列的序列, 将它"旋转"到前面. 以序列 1, 3, 5, 7, 2, 4, 6, 8为例, 此时fir=1也就是指向3, sec=5也就是指向4, maxMove=1, 通过调用t_exchange函数之后将[sec-maxMove, sec)即[4,5)中的元素也就是2"旋转"到子 序列3,5,7之前,于是该循环结束之后序列变为1,2,3,5,7,4,6,8, 此时fir=2, sec =5, 满足循环 不变量. 3) 终止: 当循环终止时, fir或者sec之一超过了数组的尺寸, 显而易见, 此时序列变成了有 序的. 完整的算法如下所示, 需要特别说明的是, 这段代码不是我想出来的, 原作者在这里: #include #include using namespace std; //int array[] = {1, 3, 5, 7, 2, 4, 6, 8}; int array[] = {3,5,7,8,1,2,4,6}; void display(int array[], int n) { for (int i = 0; i < n; ++i) { printf("%d ", array[i]); } printf("\n"); } /** * 算法: 交换二对象 **/ template void t_swap( T& v1, T& v2 ) { T t = v1; v1 = v2; v2 = t; } /** * 算法: 反转序列 **/ template void t_reverse( T* v, size_t size ) { size_t s = 0, e = size-1; while( s < e && s < size && e > 0 ) t_swap( v[s++], v[e--] ); } /** * 算法: 手摇算法,从指定位置旋转序列(见编程珠玑第二章) **/ template void t_exchange( T* v, size_t size, size_t n ) { t_reverse( v, n ); t_reverse( v + n, size - n ); t_reverse( v, size ); } /** * 算法: 合并二已排序的连续序列 **/ template void t_merge( T& v, size_t size, size_t pos ) { size_t fir = 0, sec = pos; while ( fir < sec && sec < size ) { while ( fir < sec && v[fir] <= v[sec] ) fir++; size_t maxMove = 0; while ( sec < size && v[fir] > v[sec] ) maxMove++, sec++; t_exchange( &v[fir], sec - fir, sec - fir - maxMove ); fir += maxMove; display(array, sizeof(array)/sizeof(int)); } } /** * 算法: 归并排序 **/ template void t_merge_sort( T* v, size_t size ) { if ( size <= 1 ) return; t_merge_sort( v, size/2 ); t_merge_sort( v + size/2, size - size/2 ); t_merge( v, size, size/2 ); } int main() { display(array, sizeof(array)/sizeof(int)); t_merge(array, sizeof(array) / sizeof(int), (sizeof(array)/sizeof(int))/2); //t_merge_sort(array, sizeof(array) / sizeof(int)); display(array, sizeof(array)/sizeof(int)); return 0; }
本文档为【二分查找】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_998870
暂无简介~
格式:doc
大小:61KB
软件:Word
页数:0
分类:其他高等教育
上传时间:2017-09-26
浏览量:18