算法知识-二分查找

Posted 朱芒格

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法知识-二分查找相关的知识,希望对你有一定的参考价值。

文章内容,整理自LeetCode官网题解


二分查找


基础介绍


    二分查找也称折半查找「Binary Search」,它是一种效率较高的查找方法,可以在数据规模的对数时间复杂度内完成查找。二分查找主要应用于数组结构,是因为数组具有随机访问的特点。

原理:

    将数组分为三部分,依次是中值前、中值、中值后;将要查找的值和数组的中值进行比较,若小于中值则在中值前面找,若大于中值则在中值后面找,等于中值时直接返回。然后依次是一个递归过程,将前半部分或者后半部分继续分解为三部分


应用场景

  • 查找给定的目标值在数组中是否存在

  • 查找给定的目标值在数据中的起始点

  • 二分查找算法在完全二叉树上的应用



二分查找


二分查找

LeetCode704

public int search(int[] array, int target) { int mid, left = 0; int right = array.length - 1; while (left <= right) { mid = left + ((right - left) / 2); if (array[mid] == target) { return mid; } if (array[mid] > target) { right = mid - 1; } else { left = mid + 1; } } return -1;}

注意事项:

  • 循环的条件是 while(left <= right),所以当left==right时,循环也会查找下去

  • mid = (left + right)/2; left + right > Integer.MAX_VALUE时,此时会导致数据溢出,可以采用的写法为:

    • mid = left + ((right-left)/2)

    • mid = left + ((right - left) >> 1);右移一位,相当于/2,右移n位相当于 /2的n次方



二分查找


左右区间


LeetCode34

public int[] searchRange(int[] nums, int target) { int left = binarySearchLeft(nums, target); int right = binarySearchRight(nums, target) - 1; boolean check = left <= right                 && right <= nums.length - 1  && nums[left] == target  && nums[right] == target; if (check) { return new int[]{left, right}; } else { return new int[]{-1, -1}; }}public int binarySearchLeft(int[] nums, int target) { int mid, left = 0; int right = nums.length - 1; int ans = nums.length; while (left <= right) { mid = left + (right - left) / 2; if (nums[mid] >= target) { right = mid - 1; ans = mid; } else { left = mid + 1; } } return ans;}
public int binarySearchRight(int[] nums, int target) { int mid, left = 0; int right = nums.length - 1; int ans = nums.length; while (left <= right) { mid = left + (right - left) / 2; if (nums[mid] > target) { right = mid - 1; ans = mid; } else { left = mid + 1; } } return ans;}

注意事项:

  • left的边界,第一个>=target的下标

  • right的边界,第一个>target的下标,然后减一


遇到这道题的第一思路,可能就是先利用二分查找,在数组中找到target数值。然后

  • 向左遍历,看是否存在相同的target

  • 向右遍历,看是否存在相同的target

这个想法来源于,我们最熟悉的二分查找就是用来查找某个target,如果存在,就直接返回。二分查找,最主要的特性是折半,不是仅局限于定位某个数值是否存在,还可以利用折半的特性,去搜寻边界



二分查找


完全二叉树


LeetCode222

给你一颗完全二叉树的根节点root,求出该树的节点个数。

完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为h层,则该层包含1~2^h个节点

        1            => 1 ~ 2^0  第0层
2 3 => 1 ~ 2^1 第1层
4 5 6 7 => 1 ~ 2^2 第2层

    对于任意二叉树,都可以通过广度优先搜索或深度优先搜索计算节点个数,时间复杂度和空间复杂度都是 O(n) ,其中 n 是二叉树的节点个数,实例写法如下

public int countNodes(TreeNode root){ if(root==null){ return 0; } return countNodes(root.left) + countNodes(root.right) + 1;}




    但是这里我们尝试用二分查询,来解这道题。规定根结点位于第0层,完全二叉树的最大层数为h。根据完全二叉树的特性可知,完全二叉树的最左边的节点一定位于最底层,因此从根结点出发,每次访问左子节点,直到遇到叶子节点,该叶子节点即为完全二叉树的最左边的节点,经过的路径长度即为最大层数h

    当 0<= i < h 时,第 i 层包含 2^i 个节点,最底层包含的节点数最少为1,最多为 2^h  当最底层包含1个节点时,完全二叉树的节点个数是


当最底层包含 2^h 个节点时,完全二叉树的节点个数是

    因此对于最大层数为 h 的完全二叉树,节点个数一定在 [2^h,2^{h+1} -1] 的范围内,可以在该范围内通过二分查找的方式得到完全二叉树的节点个数「这一步是不是很熟悉,得到了类似数组的左右边界,要利用二分查找,一定要先得到左右边界」

    具体做法是,根据节点个数范围的上下界得到当前需要判断的节点个数 k ,如果第 k 个节点存在,则节点个数一定大于或等于 k ,如果第 k 个节点不存在,则节点个数一定小于 k ,由此可以将查找的范围缩小一半,直到得到节点个数

    如何判断第 k 个节点是否存在呢?如果第 k 个节点位于第 h 层,则 k 的二进制表示包含 h+1 位,其中最高位是1,其余各位从高到低表示从根结点到第 k 个节点的路径,0表示移动到左子节点,1表示移动到右子节点。通过位运算得到第 k 个节点对应的路径,判断该路径对应的节点是否存在,即可判断第 k 个节点是否存在

public int countNodes(TreeNode root) { if (root == null) { return 0; } // 遍历左子树,得到高度 int level = 0; TreeNode node = root; while (node.left != null) { level++; node = node.left; } // 获得用于二分查找的开闭区间 int mid = 0; int left = 1 << level; int right = (1 << (level + 1)) - 1; while (left < right) { mid = (right - left + 1) / 2 + left; if (exists(root, level, mid)) { left = mid; } else { right = mid - 1; } } return mid;}public boolean exists(TreeNode root, int level, int k) { // 类似一个掩码,用来提取第几位数字 int bits = 1 << (level - 1); TreeNode node = root; while (node != null && bits > 0) { if ((bits & k) == 0) { node = node.left; } else { node = node.right; } // 掩码右移 bits >>= 1; } return node != null;}


算法解释 bits :

对于普通二叉树来说,使用递归的解法,就已经适用了。但是本题是完全二叉树,有一些特殊的性质,因此可以利用完全二叉树的特性来计算节点个数

bits为什么一开始是 1<< (level -1),即2^(h-1)?因为表示最底层编码的二进制一共需要h+1位。2^(h-1)二进制为:0100...0 (第一个是0,一个1,h-1个0,一共h+1位),用掩码来跳过k的第一位,只看k的后h位。非常巧妙的是,不看最底层结点的编号最高位的1,剩下数字刚好就是二叉树从根到最底层节点左0右1的路径

 

算法解释 补零 :

计算机存储数值的时候,某一个类型在内存中所占的大小是固定的,int就是32位「4字节」、long就是64位「8字节」,所以如果二进制本身长度不够这些位的话,前面就要补0

十进制2转换成二进制是1 0,如果是用int存的话,前面就要补充30个0,如下:

0000 0000 0000 0000 0000 0000 0000 0010

 

算法解释 & :

与运算符,运算规则如下:

0 & 0 = 0; 0 & 1 = 0; 1 & 1 = 1。即两个同时为1,结果为1,否则为0

例如:3&5

十进制3转换为二进制的3:0000 0011

十进制5转换为二进制的5:0000 0101

即:3&5 = 1




以上是关于算法知识-二分查找的主要内容,如果未能解决你的问题,请参考以下文章

算法知识常用算法详解丨二分查找法(折半查找)

算法:二分查找(基础)

算法入门 02二分查找(简单 - 第四题)LeetCode 167

⭐算法入门⭐《二分枚举》中等02 —— LeetCode 面试题 10.09. 排序矩阵查找

算法专题(01)二分查找(03) 简单LeetCode 278

算法专题(01)二分查找(02) 简单LeetCode 35