热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【常用算法思路分析系列】与二分搜索相关高频题

本文是【常用算法思路分析系列】的第五篇,总结二分搜索相关的高频题目和解题思路。本文分析如下几个问题:1、求数组局部最小值问题;2、元素最左出现的位置;3、循环有序数组求最小值;4、

本文是【常用算法思路分析系列】的第五篇,总结二分搜索相关的高频题目和解题思路。本文分析如下几个问题:1、求数组局部最小值问题;2、元素最左出现的位置;3、循环有序数组求最小值;4、最左原位;5、完全二叉树计算结点数;6、快速N次方

本系列前四篇导航:
【常用算法思路分析系列】排序高频题集
【常用算法思路分析系列】字符串高频题集

【常用算法思路分析系列】栈和队列高频题集(修改版)

【常用算法思路分析系列】链表相关高频题集

二分搜索的重要提醒:

一般我们选择中点进行搜索,会写成mid = (left + right) / 2;但是当数据很大的时候,下标(left + right)就可能出现溢出的情况,此时,更安全的一种写法是:mid = left + (right - left) / 2;

1、求数组局部最小值位置
定义局部最小的概念。arr长度为1时,arr[0]是局部最小。arr的长度为N(N>1)时,如果arr[0]无序数组arr,已知arr中任意两个相邻的数都不相等,写一个函数,只需返回arr中任意一个局部最小出现的位置即可。

思路:
解题思路其实就是根据局部最小的定义来求解。首先,拿到一个数组后,考虑两端的情况,初始端arr[0] arr[1],则从0位置往后(右)看,数组是递减的,如果arr[N-1] > arr[N-2],则从N-1处往前(左)看,数组也是递减的,说明符合条件的中中间位置,则考察中间位置mid = (left  + right) / 2; 类似二分搜索的划分。如下:
public int getLessIndex(int[] arr) {
        if(arr == null || arr.length == 0){
            return -1;
        }
        if(arr.length == 1 || arr[0]  arr[mid - 1]){
                right = mid - 1;
            }else if(arr[mid] > arr[mid + 1]){
                left = mid + 1;
            }else{
                return mid;
            }
        }
        return left;
    }
或者使用递归的写法,如下:    
public int getLessIndex(int[] arr) {
        if(arr == null || arr.length == 0){
            return -1;
        }
        if(arr.length == 1){
            return 0;
        }
        return getLess(arr, 0, arr.length - 1);
    }

    private int getLess(int[] arr, int left, int right) {
        if(left == right){
            return left;
        }
        if(arr[left]  arr[mid - 1]){
            return getLess(arr, left + 1, mid - 1);
        }else if(arr[mid] > arr[mid + 1]){
            return getLess(arr, mid + 1, right - 1);
        }
        return left;
    }
时间复杂度为O(logN)。
上面这个例子说明,二分搜索并不一定只在数组有序的时候才可以用,只要我们在查找的时候发现,可以留下一半,淘汰另一半,就可以使用二分搜索。

2、元素最左出现的位置

对于一个有序数组arr,再给定一个整数num,请在arr中找到num这个数出现的最左边的位置

给定一个数组arr及它的大小n,同时给定num。请返回所求位置。若该元素在数组中未出现,请返回-1。


思路:
由于数组有序,考虑用二分搜索法快速定位到数组中值为num位置处,找到后num后,因为数组有序递增,在num左边继续遍历,找到最左边的num位置,如下:    
public int findPos(int[] arr, int n, int num) {
        if(arr == null || arr.length == 1){
            return -1;
        }
        int res = -1;
        int left  = 0;
        int right = arr.length - 1;
        int mid = 0;
        while(left <= right){
            mid = (left + right) / 2;
            if(arr[mid] > num){
                right = mid - 1;
            }else if(arr[mid] 

3、循环有序数组求最小值
对于一个有序循环数组arr,返回arr中的最小值。有序循环数组是指,有序数组左边任意长度的部分放到右边去,右边的部分拿到左边来。比如数组[1,2,3,3,4],是有序循环数组,[4,1,2,3,3]也是

给定数组arr及它的大小n,请返回最小值。

测试样例:
[4,1,2,3,3],5
返回:1
思路:
(1)首先设置好下标,left=0,right=arr.length-1,如果arr[left] 
(2)当arr[left] >= arr[right]时,即数组为循环状态,开始二分搜索,找到中间位置mid = (left + right) / 2;如果arr[left] > arr[mid],说明最小值一定在mid左半部分;
(3)如果arr[mid] > arr[right],说明最小值一定出现在mid右半部分,否则,arr[left] <=arr[mid],arr[mid] <= arr[right],又arr[left] >= arr[right],由三个条件==> arr[left] == arr[right] == arr[mid]。
此时,需要用遍历的方式在left-->right的区间寻找最小值。(或者说,因为arr[left] == arr[right] == arr[mid],说明在[left,mid]区间内,必定有最小值,再在这个区间遍历搜索
public static int getMin(int[] arr, int n) {
        if(arr == null || arr.length == 0){
            return -1;
        }
        if(arr.length == 1){
            return arr[0];
        }
        int left = 0;
        int right = arr.length - 1;
        int mid = 0;
        //下面是arr[left] >= arr[right]的情形,即数组为循环状态
        while(left  arr[mid]){//最小值一定在mid左半部分
                right = mid;
                continue;
            }else if(arr[mid] > arr[right]){//最小值一定出现在mid右半部分
                left = mid;
                continue;
            }else{
                //这种情况下,arr[left] <=arr[mid],arr[mid] <= arr[right],又arr[left] >= arr[right],由三个条件==> arr[left] == arr[right] == arr[mid]
                //此时,需要用遍历的方式在left-->right的区间寻找最小值
//                int min = arr[left];
//                for(int i = left; i <= right; i++){
//                    if(arr[i] 

4、最左原位

有一个有序数组arr,其中不含有重复元素,请找到满足arr[i]==i条件的最左的位置。如果所有位置上的数都不满足条件,返回-1。

给定有序数组arr及它的大小n,请返回所求值。

测试样例:
[-1,0,2,3],4
返回:2
思路:
(1)先考察数组的两端,开始端如果arr[left] > n-1;或者,末端arr[right] <0;则直接返回-1;
(2)再通过二分搜索思想,mid = (left + right) / 2;比较arr[mid]和mid的值:
当arr[mid]>mid时,arr[i] = i的值只可能在mid左边;
当arr[mid]
当arr[mid]==mid时,继续寻找最左边arr[i] = i的值;

代码如下: 
public int findPos(int[] arr, int n, int num) {
        if(arr == null || n == 0){
            return -1;
        }
        int left = 0;
        int right = arr.length - 1;
        if(arr[left] > n - 1 || arr[right] <0){
            return -1;
        }
        int mid = 0;
        int res = -1;
        while(left <= right){
            if (arr[left] > left || arr[right]  mid){
                right = mid - 1;
            }else if(arr[mid]      
5、完全二叉树计数

给定一棵完全二叉树的根节点root,返回这棵树的节点个数。如果完全二叉树的节点数为N,请实现时间复杂度低于O(N)的解法。

给定树的根结点root,请返回树的大小。

思路:
(1)首先一直遍历到根节点的左子树的最左边,得到树的高度high;
(2)再遍历根节点的右子树的最左边,得到右子树的高度rightHigh;
(3)如果high==rightHigh,表示根节点的左子树为满二叉树,高度为high,此时左子树可以直接根据满二叉树的性质求出节点数量,再对右子树递归遍历;
(4)如果high!=rightHigh,表示根节点的右子树为满二叉树,高度为high - 1,此时右子树可以直接根据满二叉树的性质求出节点数量,再对左子树递归遍历;
代码如下:    
public int count(TreeNode root) {
        if(root == null){
            return 0;
        }
        int high = 0;
        TreeNode node = root;
        while(node != null){
            high++;
            node = node.left;
        }
        int rightHigh = 0;
        node = root;
        while(node != null){
            rightHigh++;
            node = node.left;
        }
        if(high == rightHigh){//表示根节点的左子树为满二叉树,高度为high - 1
            return (int) (Math.pow(2, high - 1)) + count(root.right);
        }else{
            return (int) (Math.pow(2, rightHigh - 1)) + count(root.left);
        }
    }

6、快速N次方

如何更快的求一个整数k的n次方。如果两个整数相乘并得到结果的时间复杂度为O(1),得到整数k的N次方的过程请实现时间复杂度为O(logN)的方法。

给定kn,请返回k的n次方,为了防止溢出,请返回结果Mod 1000000007的值。

测试样例:
2,3
返回:8
思路:
以10^13为例,首先我们将13以二进制表示如下:0000 1101
10^13 = 10^1 * 10^4 * 10^8
我们根据二进制的长度,依次获取
10^1
10^2 = 10^1 * 10^1;
10^4 = 10^2 * 10^2;
10^8 = 10^4 * 10^4;
...
在这个过程中,如果二进制位上为1,则累乘起来,代码如下:    
public int getPower(int k, int N) {
        if(k == 0){
            return 0;
        }
        if(k == 1 || N == 0){
            return 1;
        }
        long modNum = 1000000007;
        long res = 1;
        long temp = k;
        for(; N > 0; N >>= 1){
            if((N & 1) != 0){
                res *= temp;
            }
            temp = (temp * temp) % modNum;
            res = res % modNum;
        }
        return (int) res;
    }

下一篇将是与二叉树相关。


【常用算法思路分析系列】与二分搜索相关高频题


推荐阅读
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 基于layUI的图片上传前预览功能的2种实现方式
    本文介绍了基于layUI的图片上传前预览功能的两种实现方式:一种是使用blob+FileReader,另一种是使用layUI自带的参数。通过选择文件后点击文件名,在页面中间弹窗内预览图片。其中,layUI自带的参数实现了图片预览功能。该功能依赖于layUI的上传模块,并使用了blob和FileReader来读取本地文件并获取图像的base64编码。点击文件名时会执行See()函数。摘要长度为169字。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 在project.properties添加#Projecttarget.targetandroid-19android.library.reference.1..Sliding ... [详细]
  • 本文介绍了一种解析GRE报文长度的方法,通过分析GRE报文头中的标志位来计算报文长度。具体实现步骤包括获取GRE报文头指针、提取标志位、计算报文长度等。该方法可以帮助用户准确地获取GRE报文的长度信息。 ... [详细]
  • 本文介绍了django中视图函数的使用方法,包括如何接收Web请求并返回Web响应,以及如何处理GET请求和POST请求。同时还介绍了urls.py和views.py文件的配置方式。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
author-avatar
爱中华爱美丽
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有