您的位置:首页 > 其它

二分查找及其应用

2013-08-10 14:49 471 查看
(1) 二分查找:

使用二分查找(Binary Search)的前提有:(1)线性表必须是关键码有序(通常是从小到大有序)。(2)其次,线性表必须是顺序存储。所以链表不能采用二分查找。

二分查找(Binary Search)基本思想:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,知道查找成功,或者所有查找区域无记录,查找失败为止。

例题:现有这样一个有序表数组{1, 16, 24, 35, 47, 59, 62, 73, 88, 99},对它进行查找是否存在62这个数。算法实现如下:

#include <iostream>
using namespace std;

/*
函数名:	BinarySearch
函数功能:	在数组array内查找值为key的元素,返回该元素所在数组的位置
函数参数:	int *array	数组指针
int length	数组长度
int key		要查找的key值。
返回值:	如果key存在数组内,返回其在数组中的位置;否则返回-1。
*/
int BinarySearch(int *array, int length, int key)
{
/* 对参数的合法性进行判断 */
if (array == NULL || length <= 0)
{
return -1;
}

int low, middle, high;
low = 0;
high = length - 1;
while (low <= high)
{
/* middle取low和high中间元素 */
middle = low + (high - low) / 2;

/* array[middle] > key,故在middle的左边查找key */
if (key < array[middle])
{
high = middle - 1;
}
/* array[middle] < key,故在middle的右边查找key */
else if (key > array[middle])
{
low = middle + 1;
}
/* array[middle] = key,故返回middle */
else
{
return middle;
}
}

/* 如果数组中不存在key值,则返回-1 */
return -1;
}

void Test(const char *testName, int *array, int length, int key, int expectedIndex)
{
cout << testName << " : ";
if (BinarySearch(array, length, key) == expectedIndex)
{
cout << "Passed." << endl;
}
else
{
cout << "Failed." << endl;
}
}

int main()
{
int array[] = {1, 16, 24, 35, 47, 59, 62, 73, 88, 99};
Test("Test1", array, sizeof(array) / sizeof(*array), 62, 6);
Test("Test2", array, sizeof(array) / sizeof(*array), 1, 0);
Test("Test3", array, sizeof(array) / sizeof(*array), 99, 9);
Test("Test4", array, sizeof(array) / sizeof(*array), 100, -1);
Test("Test5", array, sizeof(array) / sizeof(*array), 0, -1);

return 0;
}


下面我们考虑,如何使用二分查找的基本思想来解决以下问题。

例1:把一个数组最开始的若干元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。(剑指offer,面试题8,页数:66)

旋转数组的性质有:旋转数组实际上将其分成两个子数组,前面子数组是递增的,后面子数组也是递增的,但是前面子数组的元素大于或等于后面子数组的元素,其中最小元素在后面子数组的第一个元素。例数如组{3, 4, 5, 1, 2}分成两个子数组{3,4,5}和{1, 2},则最小值为1。

我们可以尝试使用二分查找的思想,用两个指针分别指向数组的第一个元素和最后一个元素。按照题目中旋转的要求,则第一个元素应该是大于或者等于最后一个元素。

然后我们找到中间元素,如果该中间元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时数组中最小的元素应该位于该中间元素后面,所以我们把第一个指针指向该中间元素,缩小查找范围。移动之后的第一个指针仍然位于前面的递增子数组之中。

同样,如果中间元素位于后面的递增子序列,那么它应该小于或者等于第二个指针指向的元素。此时该数组中最小的元素应该位于该中间元素的前面。我们可以把第二个指针指向该中间元素,这样也可以缩小查找范围。移动之后的第二个指针仍然位于后面的递增子数组之中。

不管是移动第一个指针,还是第二个指针,查找范围都会缩小到原来的一半。接下来我们再用更新之后的两个指针,重新做新的一轮的查找。

按照上述的思路,第一个指针总是指向前面递增数组的元素,而第二个指针总是指向后面递增数组的元素。最终第一个指针将指向前面子数组的最后一个元素,而第二个指针会指向后面子数组的第一个元素。也就是它们最终会指向两个相邻的元素。而第二个指针指向刚好是最小的元素。这就是循环结束的条件。

这里有一个特殊情况,就是如果中间元素array[middle] == array[low] == array[high]这种情况,如果是这样的话,那我们就只能在low和high之间,遍历查找最小值。

整个算法实现如下:

#include <iostream>
using namespace std;

/*
函数名:	GetMinestValue
函数功能:	获取数组rotatedArray[low...high]之间最小值所在数组的位置。
函数参数:	int *rotatedArray	旋转数组
int low				查找范围为[low, high]的low
int high			查找范围为[low, high]的high
返回值:	如果存在,则返回最小值所在数组的位置;否则返回-1。
*/
int GetMinestValue(int *roatedArray, int low, int high)
{
/* 检测参数的合法性 */
if (roatedArray == NULL || low < 0 || high < 0 || low > high)
{
return -1;
}

/* 遍历数组rotatedArray[low...high],找到最小值所在的位置 */
int min = low;
for (int i = low; i <= high; i++)
{
if (roatedArray[i] < roatedArray[min])
{
min = i;
}
}

return min;
}

/*
函数名:	GetMinestValue
函数功能:	在旋转数组RotatedArray中找到最小值,返回该值所在的位置
函数参数:	int *rotatedArray	旋转数组指针
int length			旋转数组长度
返回值:	如果存在最小值,则返回该值所在位置;否则,返回-1。
*/
int GetMinestValue(int *roatedArray, int length)
{
/* 检查参数的合法性 */
if (roatedArray == NULL || length <= 0)
{
return -1;
}

int low, middle, high;
low = 0;
middle = 0;
high = length - 1;

/* 这个数组是递增的,旋转0个元素,则直接返回0 */
if (roatedArray[low] < roatedArray[high])
{
return low;
}

while (low < high)
{
/* 如果low和high是相邻的,则返回high */
if (high - low == 1)
{
middle = high;
break;;
}

middle = low + (high - low) / 2;

/* 如果中间元素和两边元素都相等的情况下,只能循环遍历数组,找到最小的值 */
if (roatedArray[low] == roatedArray[middle] && roatedArray[middle] == roatedArray[high])
{

}

/* 如果roatedArray[middle] >= roatedArray[low],说明middle在前面递增子数组,故low = middle */
if (roatedArray[middle] >= roatedArray[low])
{
low = middle;
}
/* 如果roatedArray[middle] <= roatedArray[high],说明middle在后面递增子数组,故high = middle */
else if (roatedArray[middle] <= roatedArray[high])
{
high = middle;
}
}

return middle;
}

void Test(const char *testName, int *rotatedArray, int length, int expectedIndex)
{
cout << testName << " : ";
if (GetMinestValue(rotatedArray, length) == expectedIndex)
{
cout << "Passed." << endl;
}
else
{
cout << "Failed." << endl;
}
}

int main()
{
int rotatedArray1[] = {1, 2, 3, 4, 5};
Test("Test1", rotatedArray1, sizeof(rotatedArray1) / sizeof(int), 0);
int rotatedArray2[] = {3, 4, 5, 1, 2};
Test("Test2", rotatedArray2, sizeof(rotatedArray2) / sizeof(int), 3);
int rotatedArray3[] = {1, 1, 1, 0, 1};
Test("Test3", rotatedArray3, sizeof(rotatedArray3) / sizeof(int), 3);
int rotatedArray4[] = {5};
Test("Test4", rotatedArray4, sizeof(rotatedArray4) / sizeof(int), 0);
return 0;
}


例2:统计一个数字在排序数组中出现的次数。例如输入排序数组{1, 2, 3, 3, 3, 3, 4, 5}和数字3,由于3在这个数组中出现了4次,因此输出4。(剑指offer,面试题38,页数204)

如何查找排序数组中k的个数,首先我们分析如何用二分查找算法在数组中找到第一个k,二分查找算法总是先拿数组中间的数字和k作比较。如果中间的数组比k大,那么k只有可能出现在数组的前半段,下一轮我们只在数组的前半段查找就可以了。如果中间的数字比k小,那么k只有可能出现在数组的后半段,下一轮我们只在数组的后半段查找就可以了。那么如果中间的数字和k相等呢?我们先判断这个数字是不是第一个k。如果位于中间数字的前面一个数字不是k,此时中间的数字刚好就是第一个k。如果位于中间数字的前面一个数字也是K,也就是说第一个k肯定在数组的前半段,下一轮我们仍然需要在数组的前半段查找。

基于这个思路,我们很容易实现该算法找到第一个k:

/*
函数名:	GetFirstK
函数功能:	从排序数组sortedArray[start ... end]中寻找第一个值为k的位置。
函数参数:	int *sortedArray	排序数组指针
int k				即要查找的数字k
int start			[start,end]起始位置start
int end				[start,end]起始位置end
返回值:	如果找到k值,则返回第一个该值所在的位置;否则返回-1.
*/
int GetFirstK(int *sortedArray, int k, int start, int end)
{
if (sortedArray == NULL || start < 0 || end < 0 || start > end)
{
return -1;
}

/* 如果在[start,end]范围内,第一个数字是k,则返回start */
if (sortedArray[start] == k)
{
return start;
}

while (start <= end)
{
int middle = start + (end - start) / 2;

/* 如果sortedArray[middle] < k,则k在数组的下半部分,所以start = middle + 1 */
if (sortedArray[middle] < k)
{
start = middle + 1;
}
/* 如果sortedArray[middle] > k,则k在数组的上半部分,所以end = middle - 1 */
else if (sortedArray[middle] > k)
{
end = middle - 1;
}
/* 如果sortedArray[middle] == k,则判断sortedArray[middle-1]是否等于k。
如果sortedArray[middle-1] == k,则第一个k值在前半部分,则end = middle-1。
如果sortedArray[middle-1] != k,则第一个k值就是middle,则返回middle
*/
else
{
if (sortedArray[middle - 1] == k)
{
end = middle - 1;
}
else
{
return middle;
}
}
}

return -1;
}


然后我们用同样的思路在排序数组中找到最后一个k。如果中间数字比k大,那么k只能出现在数组的前半段。如果中间数组比k小,k就只能出现在数组的后半部分。如果中间数字等于k呢?我们需要判断这个k是不是最后一个k,也就是中间数字的下一个数字是不是也等于k。如果下一个数字不是k,则中间数字就是最后一个k了;否则下一轮我们还是要在数组的后半段中去查找。

基于这个思路,我们很容易实现该算法找到最后一个k:

/*
函数名:	GetLastK
函数功能:	从一个有序数组sortedArray[start...end]中找到最后一个k值所在的位置。
函数参数:	int *sortedArray	有序数组指针
int k				要查找的k值
int start			[start,end]的起始start
int end				[start,end]的结束end
返回值:	如果k值存在于数组中,返回最后一个k值所在的位置;否则返回-1。
*/
int GetLastK(int *sortedArray, int k, int start, int end)
{
/* 判断参数的合法性 */
if (sortedArray == NULL || start < 0 || end < 0 || start > end)
{
return -1;
}

/* 如果最后一个数是k,则返回end */
if (sortedArray[end] == k)
{
return end;
}

while (start <= end)
{
int middle = start + (end - start) / 2;
/* 如果sortedArray[middle]小于k,则k将出现在数组的后半段,则start = middle + 1 */
if (sortedArray[middle] < k)
{
start = middle + 1;
}
/* 如果sortedArray[middle]大于k,则k将出现在数组的后半段,则end = middle - 1 */
else if (sortedArray[middle] > k)
{
end = middle - 1;
}
/* 如果sortedArray[middle] == k,则判断sortedArray[middle+1]是否等于k。
如果sortedArray[middle+1] == k,则最后一个k值在后半段,则start = middle + 1。
如果sortedArray[middle+1] != k,则最后一个k值就是middle,则返回middle。
*/
else
{

if (sortedArray[middle + 1] == k)
{
start = middle + 1;
}
else
{
return middle;
}
}
}

return -1;
}


最后我们通过最后一个k值所在位置,和第一个k值所在位置,就能得到k值的次数。

/*
函数名:	GetNumberOfK
函数功能:	从一个排序数组中,获取k出现的次数
函数参数:	int *sortedArray	排序数组首地址
int length			数组长度
int k				要查找的值k
返回值:	如果k值存在于数组中,则返回k出现的次数;否则返回0。
*/
int GetNumberOfK(int *sortedArray, int length, int k)
{
if (sortedArray == NULL || length <= 0)
{
return 0;
}

int first = GetFirstK(sortedArray, k, 0, length - 1);
int last = GetLastK(sortedArray, k, 0, length - 1);
if (first == -1 && last == -1)
{
return 0;
}
else
{
return last - first + 1;
}
}


最后测试代码和主函数如下:

void Test(const char *testName, int *sortedArray, int length, int k, int expectedCount)
{
cout << testName << " : ";
if (GetNumberOfK(sortedArray, length, k) == expectedCount)
{
cout << "Passed." << endl;
}
else
{
cout << "Failed." << endl;
}
}

int main()
{
int sortedArray1[] = {1, 2, 3, 3, 3, 3, 4, 5};
Test("Test1", sortedArray1, sizeof(sortedArray1) / sizeof(int), 3, 4);

int sortedArray2[] = {3, 3, 3, 3, 4, 5};
Test("Test2", sortedArray2, sizeof(sortedArray2) / sizeof(int), 3, 4);

int sortedArray3[] = {1, 2, 3, 3, 3, 3};
Test("Test3", sortedArray3, sizeof(sortedArray3) / sizeof(int), 3, 4);

int sortedArray4[] = {1, 3, 3, 3, 3, 4, 5};
Test("Test4", sortedArray4, sizeof(sortedArray4) / sizeof(int), 2, 0);

int sortedArray5[] = {1, 3, 3, 3, 3, 4, 5};
Test("Test5", sortedArray5, sizeof(sortedArray5) / sizeof(int), 0, 0);

int sortedArray6[] = {1, 3, 3, 3, 3, 4, 5};
Test("Test6", sortedArray6, sizeof(sortedArray6) / sizeof(int), 6, 0);

int sortedArray7[] = {3, 3, 3, 3};
Test("Test7", sortedArray7, sizeof(sortedArray7) / sizeof(int), 3, 4);

int sortedArray8[] = {3, 3, 3, 3};
Test("Test8", sortedArray8, sizeof(sortedArray8) / sizeof(int), 4, 0);

int sortedArray9[] = {3};
Test("Test9", sortedArray9, sizeof(sortedArray9) / sizeof(int), 3, 1);

int sortedArray10[] = {3};
Test("Test10", sortedArray10, sizeof(sortedArray10) / sizeof(int), 4, 0);

Test("Test11", NULL, 0, 0, 0);

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C 算法 二分查找