美文网首页
TS数据结构与算法之二分查找

TS数据结构与算法之二分查找

作者: 子十一刻 | 来源:发表于2022-03-02 09:43 被阅读0次

什么是二分查找

有序数据集合对于查找算法是很有利的。在有序集合中顺序查找时,当与第一个项进行比较,如果第一项不是要查找的,则还有 n-1 项待比较。当遇到超过范围的值时,可以停止查找,综合来看这种有序查找速度还是比较慢。对于排好序的数据集有没有更快的查找算法呢?当然有,那就是二分查找。

其实二分查找的意思体现在其名字上了,说白了就是把数据集分成两部分来查找,通过start,mid,end 来控制查找的范围。从中间项 mid 开始,而不是按顺序查找。如果中间项是正在寻找的项,则完成了查找。如果它不是,可以使用排序集合的有序性质来消除一半剩余项。如果正在查找的项大于中间项,就可以消除中间项以及比中间项小的一半元素,也就是第一项到中间项都可以不用去比较了。因为目标项大于中间值,那么不管是否在集合中,那肯定不会在前一半。相反,若是目标项小于中间项,则后面的一半数据就都可以不用比较了。去除了一半数据后在剩下的一半数据项里再查看其中间项,重复上述的比较和省略过程,最终得到结果。这个查找速度是比较快的,而且也非常形象,所以叫二分查找。

实现原理及代码

    19  20  26   33   44   56   58   66  72  92
    |                 |                      |
  start              mid                    end

二分查找示意图如上图。初始时 start 和 end 分居最左和最右。比如你要查60,那么先和中间值比较,大于44,则 start 移动到56 处,mid 移动到66。此时和 66 比较,发现小于66,所以 end 移动到58,mid 移动到56 和 start 重合了。比较发现大于56,所以 start 移动到58,mid 也移动到58,发现大于58。此时 start, end, mid 三者在一个位置。接着 start 移动到66,发现下标大于end 了,不满足条件,查找停止,退出。

循环方式

根据上面的描述和示意图,可以写出如下的二分查找代码。

/**
 * 使用循环的方式实现二分查找
 * 
 * 10  20  30 40 50 60 70
 * start      min      end
 * 
 * @param arr 要查找的数组
 * @param target 目标数据
 * @return number 目标数据索引下标
 */
export const binarySearch1 = (arr: number[], target: number): number => {
  const len = arr.length;
  if (!len) return -1;

  // 起始索引
  let startIndex = 0;
  // 结束索引
  let endIndex = len - 1;
  
  while (startIndex <= endIndex) {
    let midIndex = Math.floor((startIndex + endIndex) / 2);
    let midTarget = arr[midIndex];

    if (target < midTarget) {
      // 目标小于中值 舍弃右半部分
      endIndex = midIndex - 1;
    } else if (target > midTarget) {
       // 大于中值 舍弃左半部分
       startIndex = midIndex + 1;
    } else if (target === midTarget) {
      // 相同找到目标直接返回下标
      return midIndex;
    }
  }
  // 循环结束没找到
  return -1;
};

递归方式

二分法其实是把大问题分解成小问题,采取的是分而治之策略。以前了解过类似这种分解大问题为小问题可以考虑用递归来解决,那二分法和递归是否有相似,二分法是否能用递归实现呢?我们发现二分查找时,找到或没找到是最终的结果,是一个基本情况。而二分法不断减小问题的尺度,不断向基本情况靠近,且二分法是在不断重复自身步骤。所以二分法满足递归三定律,所以可以用递归实现二分法,具体实现如下。

/**
 * 使用递归的方式实现二分查找
 * 
 * @param arr 要查找的数组
 * @param target 目标数据
 * @param startIndex 起始索引
 * @param endIndex 结束索引
 * @return number 目标数据索引下标
 */
export const binarySearch2 = (
  arr: number[],
  target: number,
  startIndex?: number,
  endIndex?: number
): number => {
  const len = arr.length;
  if (!len) return -1;

  // 初始化起始索引
  let start = startIndex || 0;
  let end = endIndex || len - 1;
  
  if (start > end) return -1;

  // 计算当前中值索引
  let midIndex = Math.floor((start + end) / 2);
  const midTarget = arr[midIndex];

  if (target < midTarget) {
    // 目标小于中值 舍弃右半部分
    return binarySearch2(arr, target, start, midIndex - 1);
  } else if (target > midTarget) {
    // 大于中值 舍弃左半部分
    return binarySearch2(arr, target, midIndex + 1, end);
  } else if (target === midTarget) {
    // 相同找到目标直接返回下标
    return midIndex;
  }

  return -1;
}

递归算法总是涉及栈的使用,有爆栈风险,一般来说二分查找最好用循环迭代法来解决。

功能测试

/**
 * 功能测试
 */
export const binarySearch1FunctionalTest = () => {
  const arr = [10, 20, 30, 40, 50];
  console.log(binarySearch2(arr, 20));  // 1
  console.log(binarySearch2(arr, 30));  // 2
  console.log(binarySearch2(arr, 50)); // 4
  console.log(binarySearch2(arr, 80));  // -1
  console.log(binarySearch2(arr, 0));   // -1
}

单元测试

文件: tests/binary-search.test.ts

/**
 * 二分查找单元测试
 */
import { binarySearch1, binarySearch2 } from '../src/utils';

describe('二分查找单元测试', () => {
  test('循环方式正常测试', () => {
    const arr = [10, 20, 30, 40, 50, 60];
    expect(binarySearch1(arr, 10)).toBe(0);
    expect(binarySearch1(arr, 30)).toBe(2);
    expect(binarySearch1(arr, 40)).toBe(3);
    expect(binarySearch1(arr, 80)).toBe(-1);
    expect(binarySearch1(arr, -100)).toBe(-1);
  });

  test('递归方式正常测试', () => {
    const arr = [10, 20, 30, 40, 50, 60];
    expect(binarySearch2(arr, 10)).toBe(0);
    expect(binarySearch2(arr, 30)).toBe(2);
    expect(binarySearch2(arr, 40)).toBe(3);
    expect(binarySearch2(arr, 80)).toBe(-1);
    expect(binarySearch2(arr, -100)).toBe(-1);
  });

  test('空数据', () => {
    expect(binarySearch1([], 10)).toBe(-1);
    expect(binarySearch2([], 10)).toBe(-1);
  });
});

单元测试执行结果(测试通过):

PASS  tests/binary-search.test.ts (8.263 s)
  二分查找单元测试
    ✓ 循环方式正常测试 (3 ms)
    ✓ 递归方式正常测试
    ✓ 空数据 (1 ms)

性能测试

循环使用两种查询方式操作100W次

/**
 * 性能测试
 */
export const binarySearchPerformanceTest = () => {
  const arr = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 150, 200, 300, 400];
  console.time('binarySearch1Time');
  for (let i = 0; i < 100 * 10000; i++) {
    binarySearch1(arr, 30);
  }
  console.timeEnd('binarySearch1Time'); 


  console.time('binarySearch2Time');
  for (let i = 0; i < 100 * 10000; i++) {
    binarySearch2(arr, 30);
  }
  console.timeEnd('binarySearch2Time'); 
}

输出结果:

  • binarySearch1Time: 12.342041015625 ms
  • binarySearch2Time: 25.17919921875 ms

二分查找复杂度

二分查找算法最好的情况是中间项就是目标,此时复杂度为 O(1)。二分查找每次比较能消除一半的剩余项,可以计算最多的比较次数得到该算法的最差复杂度。第一次比较后剩 n/2,第二次比较后剩余 n/4,直到 n/8,n/16,n/2i 等等。当 n/2i = 1 时,二分结束。所以

\frac{n}{2^i} = 1

i = log_2(n)

所以二分查找算法最多比较 log_2(n) 次,复杂度也就是 O(log_2(n)), 这是一个比 O(n) 算法还要优秀的算法。但要注意,在上述的递归版实现中,默认的栈使用是会消耗内存的,复杂度不如循环版。

二分查找看起来很好,但如果 n 很小,就不值得去排序再使用二分,此时直接使用顺序查找可能复杂度还更好。此外,对于很大数据集,对其排序很耗时和耗内存,那么直接采用顺序查找复杂度可能也更好。好在实际项目中大量的数据集即不多也不少,非常适合二分查找。

相关文章

  • TS数据结构与算法之二分查找

    什么是二分查找 有序数据集合对于查找算法是很有利的。在有序集合中顺序查找时,当与第一个项进行比较,如果第一项不是要...

  • 排序算法

    算法与数据结构基础 查找算法: 二分查找法: 简介:二分查找法又被称为折半查找法,用于预排序的查找问题 过程: 如...

  • 数据结构与算法--散列表

    数据结构与算法--散列表 之前学习了基于链表的顺序查找、基于有序数组的二分查找、二叉查找树、红黑树,这些算法在查找...

  • 数据结构与算法 树 引言 顺序查找 ​ 哨兵的方式 ​ 时间复杂度为O(N) 二分查找查找树的形式我...

  • 常考的数据结构与算法之算法

    本文记录一下我学习数据结构与算法中算法的知识点,使用的语言是C/C++语言 查找 二分查找又叫折半查找,要求线性表...

  • 数据结构与算法--Boyer-Moore和Rabin-Karp子

    数据结构与算法--Boyer-Moore和Rabin-Karp子字符串查找 Boyer-Moore字符串查找算法 ...

  • 算法草稿

    常用算法集合 字符处理算法数组与查找链表树算法思路 递归、动态规划、BFS/DFS、双指针、二分法搜索数据结构的...

  • 查找算法之二分查找算法

    二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其是要求待查表为有序表,且插入删除困难。因此,折半...

  • 查找

    二分查找适用于有序表查找, 包括二叉排序树 散列查找请看本博客数据结构与算法hash表文章http://www.j...

  • 带领初学者学习数据结构与算法免费视频教程

    带领初学者学习数据结构与算法免费视频教程(2 个视频) 详解使用 JavaScript 实现二分查找算法「12:3...

网友评论

      本文标题:TS数据结构与算法之二分查找

      本文链接:https://www.haomeiwen.com/subject/bassrrtx.html