int bisearch(int a[], int n, int t) //数组a有序,长度为n, 待查找的值为t { int l = 0, u = n - 1; while (l <= u) { int m = l + (u - l) / 2; //同(l+u)/ 2,这里是为了溢出 if (t > a[m]) l = m + 1; else if (t < a[m]) u = m - 1; else return m; } return -(l+1); }算法的思想就是:从数组中间开始,每次排除一半的数据,时间复杂度为O(lgN)。 这依赖于数组有序这个性质。如果t存在数组中,则返回t在数组的位置;否则,不存在则返回-(l+1)。
int bsearch_first(int a[], int n, int t) { int l = -1, u = n; while (l + 1 != u) { /*循环不变式a[l]<t<=a[u] && l<u*/ int m = l + (u - l) / 2; //同(l+u)/ 2 if (t > a[m]) l = m; else u = m; } /*assert: l+1=u && a[l]<t<=a[u]*/ int p = u; if (p>=n || a[p]!=t) p = -1; return p; }算法分析:设定两个不存在的元素a[-1]和a[n],使得a[-1] < t <= a[n],但是我们并不会去访问者两个元素,因为(l+u)/2 > l=-1, (l+u)/2 < u=n。循环不变式为l<u && t>a[l] && t<=a[u] 。循环退出时必然有l+1=u, 而且a[l] < t <= a[u]。循环退出后 u的值为t可能出现的位置,其范围为[0, n],如果t在数组中,则第一个出现的位置p=u,如果不在,则设置p=-1返回。该算法的效率虽然解决了更为复杂的问题,但是其效率比初始版本的二分查找还要高,因为它在每次循环中只需要比较一次,前一程序则通常需要比较两次。
int bsearch_last(int a[], int n, int t) { int l = -1, u = n; while (l + 1 != u) { /*循环不变式, a[l] <= t < a[u]*/ int m = l + (u - l) / 2; if (t >= a[m]) l = m; else u = m; } /*assert: l+1 = u && a[l] <= t < a[u]*/ int p = l; if (p<=-1 || a[p]!=t) p = -1; return p; }
int binary_search_first_last(int arr[], int p, int q, int value, bool firstflag = true) { int begin = p; int end = q; while(begin <= end) { int mid = (begin + end)/2; if(arr[mid] == value) //找到了,判断是第一次出现还是最后一次出现 { if(firstflag) //查询第一次出现的位置 { if(mid != p && arr[mid-1] != value) return mid; else if(mid == p) return p; else end = mid - 1; } else //查询最后一次出现的位置 { if(mid != q && arr[mid+1] != value) return mid; else if(mid == q) return q; else begin = mid + 1; } } else if(arr[mid] < value) begin = mid + 1; else end = mid - 1; } return -1; }
题目:把一个有序数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。例如数组{3, 4, 5, 1, 2}为{1, 2, 3, 4, 5}的一个旋转。现在给出旋转后的数组和一个数,旋转了多少位不知道,要求给出一个算法,算出给出的数在该数组中的下标,如果没有找到这个数,则返回-1。要求查找次数不能超过n。
分析:由题目可以知道,旋转后的数组虽然整体无序了,但是其前后两部分是部分有序的。由此还是可以使用二分查找来解决该问题的。
解法一::2次二分查找。首先确定数组分割点,也就是说分割点两边的数组都有序。比如例子中的数组以位置2分割,前面部分{3,4,5}有序,后半部分{1,2}有序。然后对这两部分分别使用二分查找即可。代码如下:
int split(int a[], int n) { for (int i=0; i<n-1; i++) { if (a[i+1] < a[i]) return i; } return -1; } int bsearch_rotate(int a[], int n, int t) { int p = split(a, n); //找到分割位置 if (p == -1) return bsearch_first(a, n, t); //如果原数组有序,则直接二分查找即可 else { int left = bsearch_first(a, p+1, t); //查找左半部分 if (left == -1) { //左半部分没有找到,则查找右半部分 int right = bsearch_first(a+p+1, n-p-1, t); //查找右半部分 if (right != -1) return right+p+1; //返回位置,注意要加上p+1 return -1; } return left; //左半部分找到,则直接返回 } }
二分查找算法有两个关键点:1)数组有序;2)根据当前区间的中间元素与x的大小关系,确定下次二分查找在前半段区间还是后半段区间进行。
仔细分析该问题,可以发现,每次根据low和high求出mid后,mid左边([low, mid])和右边([mid, high])至少一个是有序的。
a[mid]分别与a[left]和a[right]比较,确定哪一段是有序的。
如果左边是有序的,若x<a[mid]且x>a[left], 则right=mid-1;其他情况,left =mid+1;
如果右边是有序的,若x> a[mid] 且x<a[right] 则left=mid+1;其他情况,right =mid-1;
代码如下:
int bsearch_rotate(int a[], int n, int t) { int low = 0, high = n-1; while (low <= high) { int mid = low + (high-low) / 2; if (t == a[mid]) return mid; if (a[mid] >= a[low]) { //数组左半有序 if (t >= a[low] && t < a[mid]) high = mid - 1; else low = mid + 1; } else { //数组右半段有序 if (t > a[mid] && t <= a[high]) low = mid + 1; else high = mid - 1; } } return -1; }