数据结构和算法-15-二分查找

知识共享许可协议 版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons

前面学习了六种排序算法,接着学习搜索,搜索中使用最多一种简单查找方法就是二分查找。二分查找的特点是,先保证数列是有序排序,然后每次查找可以减少一半的范围,直到查到或者找不到目标元素为止。这个也是经常在面试中被要求手写这个查找代码,接着要你设计测试用例去测试你写的代码。

1.二分查找定义

二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好。其缺点就是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置纪录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置纪录将表分成前后两个子表。如果中间位置纪录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的纪录,使查找成功,或者直到子表不存在为止,此时查找不成功。

2.二分查找图解

例如有下面一个数列,二分查找算法如下,上半部分是二分查找,下半部分是顺序查找。

二分查找的好处,每次查询一遍之后,接下来要查找范围缩小了一般。上图刚好是二分查找的最坏情况和顺序查找的最优情况对比。

3.二分查找代码实现

Python代码实现

先来看看递归的方式实现

# coding:utf-8


def binary_search(alist, item):
    """二分查找"的递归实现"""
    n = len(alist)

    if n > 0:
        mid = n // 2
        if alist[mid] == item:
            return True
        elif item < alist[mid]:
            return binary_search(alist[:mid], item)
        else:
            return binary_search(alist[mid+1:], item)

    return False


if __name__ == "__main__":
    li = [1, 3, 6, 7, 11, 20, 39]
    print(binary_search(li, 39))
    print(binary_search(li, 44))

运行结果

True
False

再来看看第二种方式,非递归方法

# coding:utf-8


def binary_search(alist, item):
    """二分查找"的非递归实现"""
    n = len(alist)
    first = 0
    last = n-1

    while first <= last:
        mid = (first + last) // 2
        if alist[mid] == item:
            return alist.index(item)
        elif alist[mid] < item:
            first = mid + 1
        else:
            last = mid - 1
    return -1


if __name__ == "__main__":
    li = [1, 3, 6, 7, 11, 20, 39]
    print(binary_search(li, 39))
    print(binary_search(li, 44))

上面的设计是如果找到了就返回该元素在数列中的下标也就是索引,找不到返回-1.

运行结果

6
-1

Java代码实现

第一种递归实现

package com.anthony.test;

import java.util.Arrays;

public class BinarySearch {

    public static void main(String[] args) {
        int[] arr = {1, 3, 6, 7, 11, 20, 39};
        System.out.println(binarySeach_01(arr, 39));
        System.out.println(binarySeach_01(arr, 44));
    }

    public static boolean binarySeach_01(int[] arr, int item) {
        int n = arr.length;

        if(n > 0){
            int mid = n / 2;
            if ( item == arr[mid]){
                return true;
            }else if(item < arr[mid]) {
                return binarySeach_01(Arrays.copyOfRange(arr,0, mid -1), item);
            } else{
                return binarySeach_01(Arrays.copyOfRange(arr,mid+1, n), item);
            }
        }
        return false;
    }
}

第二种非递归实现

package com.anthony.test;

import java.util.Arrays;

public class BinarySearch {

    public static void main(String[] args) {
        int[] arr = {1, 3, 6, 7, 11, 20, 39};
        System.out.println(binarySeach_02(arr, 39));
        System.out.println(binarySeach_02(arr, 44));
    }

    public static int binarySeach_02(int[] arr, int item) {
        
        //1.定义最小索引,最大索引,中间索引的标记
        int max = arr.length - 1;
        int min = 0; 
        int mid = (min+max)/2;
        
        //2 当中间值不等于要找的值,就开始循环
        while (arr[mid] != item) {
            if(arr[mid] < item) {
                // 说明目标元素在右半部分,最小的索引改变
                min = mid + 1;
            }else if (arr[mid] > item) {
                // 说明目标元素在左侧半部分,最大的索引改变
                max = mid - 1;
            }
            // 由于上面min或者max发生了改变,所以mid需要重新获取新的值
            mid = (min + max)/2;
            // 如果最小索引大于最大索引就没有查找的可能性,返回-1
            if(min > max) {
                return -1;
            }
        }
        return mid;

    }
}

运行结果

6
-1

4.针对上面java版本非递归方法的单元测试

这个题目,我在滴滴面试过程中遇到过,当时每考虑全测试点。

测试点1:100%语句覆盖

因为是白盒测试,这里先来一个百分百语句覆盖的测试用例。我们二分查找的思路就是,先和中间元素比较,这是一个代码分支,然后比较左半部分,这是第二个代码分支测试点,然后是右半部分列表去查找,这是第三个代码分支测试点。所以,我们先来一个只有三个元素的数列,然后分别去查找三次,第一次查找第一个元素代表左半部分代码路径覆盖,第二次查找中间元素,这个时候刚好覆盖arr[mid]== item这个代码分支,第三次查找第三个元素,模拟右半部分数列的二分查找。,第四次查找模拟查找不到的情况。三次查找,四个测试用例,我们在一个junit的方法中覆盖。

把Java第二种方法的二分查找写到一个类中,作为静态工具类使用。

package test;

import org.junit.Test;

public class TestBinarySearch {
	
	@Test
	public void test1() {
		System.out.println("100%代码路径覆盖测试");
		int[] arr = {1, 2, 3};
		int item1 = 1;
		int item2 = 2;
		int item3 = 3;
		int item4 = 4;
		System.out.println(BinarySearch.binarySeach_02(arr, item1));
		System.out.println(BinarySearch.binarySeach_02(arr, item2));
		System.out.println(BinarySearch.binarySeach_02(arr, item3));
		System.out.println(BinarySearch.binarySeach_02(arr, item4));
	}

}

运行结果

100%代码路径覆盖测试
0
1
2
-1

测试点2:分支覆盖测试

我们这里代码分支,有两个,一个是元素在左半部分,第二个是元素在右半部分。所以,这里我们测试用例设计没有上面这个用例考虑全面,这里我们只是测试if -else这两个分支,下面用例代表左半部分元素查找和右半部分查找的使用场景。

@Test
public void test2() {
	System.out.println("分支覆盖测试");
	int[] arr = {1, 2, 3, 4, 5, 6, 7, 9};
	int item1 = 2;
	int item2 = 7;
	System.out.println(BinarySearch.binarySeach_02(arr, item1));
	System.out.println(BinarySearch.binarySeach_02(arr, item2));
}

测试点3:谓词完全覆盖测试

这个谓词覆盖,我也是第一次听说,这种概念的东西,其实不实用。简单来说代码中谓词就是 !=, > <这样的代码。所以,下面设计用例其实和上面分支覆盖是一样的用例。

@Test
public void test3() {
	System.out.println("谓词覆盖测试");
	int[] arr = {1, 2, 3, 4, 5, 6, 7, 9};
	int item1 = 0;
	int item2 = 6;
	System.out.println(BinarySearch.binarySeach_02(arr, item1));
	System.out.println(BinarySearch.binarySeach_02(arr, item2));
}

上面查找0,覆盖了while 中的!=这个判断,查找6覆盖了分支中大于和小于的判断。

测试点4:缺陷测试(没有完整覆盖路径)

缺陷就是用例只覆盖了代码中一部分代码,例如一个列表,我们只查找一个元素,肯定一次执行不能覆盖全部代码。

@Test
public void test4() {
	System.out.println("有缺陷");
	int[] arr = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18};
	int item1 = 11;
	System.out.println(BinarySearch.binarySeach_02(arr, item1));
}

为了解决这个缺陷问题,我们可以写一个依次查找列表中每一个元素和一个不存在的元素,也能覆盖全部代码路径。

@Test
public void test5() {
	System.out.println("没缺陷的覆盖查询");
	int[] arr = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18};
	int item1 = 0;
	System.out.println(BinarySearch.binarySeach_02(arr, item1));
	for (int i : arr) {
		System.out.println(BinarySearch.binarySeach_02(arr, i));
	}
}

猜你喜欢

转载自blog.csdn.net/u011541946/article/details/93631967