跳至主要內容

hot150-15-二分查找

holic-x...大约 24 分钟算法算法

难度说明:🟢简单🟡中等🔴困难

hot150-15-二分查找

二分查找通用规律

    /**
     * 范围查询规律
     * 初始化:
     *   int left = 0;
     *   int right = nums.length - 1;
     * 循环条件
     *   left <= right
     * 右边取值
     *   right = mid - 1
     * 左边取值
     *   left = mid + 1
     * 查询条件
     *   >= target值, 则 nums[mid] >= target时, 都减right = mid - 1
     *   >  target值, 则 nums[mid] >  target时, 都减right = mid - 1
     *   <= target值, 则 nums[mid] <= target时, 都加left = mid + 1
     *   <  target值, 则 nums[mid] <  target时, 都加left = mid + 1
     * 结果
     *   求大于(含等于), 返回left
     *   求小于(含等于), 返回right
     * 核心思想: 要找某个值, 则查找时遇到该值时, 当前指针(例如right指针)要错过它, 让另外一个指针(left指针)跨过他(体现在left <= right中的=号), 则找到了
     */

🚀二分查找区间技巧

注意点

(1)数据溢出问题

​ 中点取值int mid = (left + right)/2存在数据溢出问题,因此调整为int mid = left + (right - left)/2(先减后加,避免相加后数据太大导致数据溢出问题)

(2)循环退出条件

​ 当查找区间为空的时候循环结束

(3)区间取值问题(不同区间取值的处理)

​ 二分法需注意,区间的定义在于区间内的数(下标)都是还没确定与target的大小关系的,区间外的数是确定了与target的大小关系

  • 初始区间取值
  • 循环退出条件
  • 区间选择

(1)闭区间([left,right]

// 闭区间
public int binarySearch1(int[] nums, int target) {
    // 定义检索区间
    int left = 0, right = nums.length - 1; // 闭区间
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (target < nums[mid]) {
            // 如果目标在左侧,则右侧缩边
            right = mid - 1;
        } else if (target > nums[mid]) {
            // 如果目标在右侧,则左侧缩边
            left = mid + 1;
        } else {
            return mid;
        }
    }
    // 返回元素可插入的下个位置(如果不存在可以直接返回-1)
    return left; // return -1;
}
public int binarySearch1(int[] nums, int target) {
    // 定义检索区间
    int left = 0, right = nums.length - 1; // 闭区间
    while (left <= right) { // 区间不为空时
        int mid = left + (right - left) / 2;
        if (nums[mid]<target) {
            left = mid + 1; // 取[mid+1,right]区间
        } else{
            right = mid -1; // 取[left,mid-1]区间 (target==nums[mid]的情况也包括在内)
        }
    }
    // 返回元素可插入的下个位置(如果不存在可以直接返回-1)
    return left; // return -1;
}

(2)左闭右开([left,right)

// 左闭右开区间
public int binarySearch2(int[] nums, int target) {
    // 定义检索区间
    int left = 0, right = nums.length; // 闭区间
    while (left < right) { // 区间不为空时
        int mid = left + (right - left) / 2;
        if (nums[mid]<target) {
            left = mid + 1; // 取[mid+1,right)区间
        } else{
            right = mid ; // 取[left,mid)区间 (target==nums[mid]的情况也包括在内)
        }
    }
    // 返回查找到的target所在索引
    return left; // return right;
}

(3)开区间((left,right)

// 左闭右开区间
public int binarySearch3(int[] nums, int target) {
    // 定义检索区间
    int left = -1, right = nums.length; // 开区间(左开右开)
    while (left + 1 < right) { // 区间不为空时
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid; // 取(mid,right)区间
        } else {
            right = mid; // 取(left,mid)区间 (target==nums[mid]的情况也包括在内)
        }
    }
    // 返回查找到的target所在索引
    return right;
}

🟢01-搜索插入位置(35)

1.题目内容

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

2.题解思路

👻方法1:二分查找(O(logN))🚀(二分查找思路)

public class Solution2 {
    public int searchInsert(int[] nums, int target) {
        int len = nums.length;
        int start = 0, end = len - 1;
        while (start <= end) {
            // 获取中间点
            int mid = (start + end) / 2; // 注意梳理数据溢出问题,先减后加:int mid = left + (right-left)/2
            // 判断target所在区间
            if (target < nums[mid]) {
                // target 在左区间,右指针缩边
                end = mid - 1;
            } else if (target == nums[mid]) {
                return mid;
            } else if (target > nums[mid]) {
                // target 在右区间,左指针缩边
                start = mid + 1;
            }
        }
        return start;
    }
}
  • 复杂度分析

    • 时间复杂度:O(logN)
  • 空间复杂度:O(1) 常数级别的检索

另一种写法

public int searchInsert(int[] nums, int target) {
    int len = nums.length;
    int left = 0, right = len - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2; // 中间节点的索引位置
        // 判断target在哪个区间
        if (target <= nums[mid]) {
            // target在左区间,right缩边
            right = mid - 1;
        } else {
            // target在右区间,left缩边
            left = mid + 1;
        }
    }
    return left;
}

🟡02-搜索二维矩阵(74)

解题思路

  • 二维转一维:先定位元素所在行,然后对指定行进行检索
    • 【思路1】:列、行均二分法:均使用二分法,先定位元素所在行(注意数组边界处理),然后对指定行进行检索(回归普通一维数组的按行检索)
    • 【思路2】:遍历每一行,每一行再单独按行检索
  • 规律法
    • 【思路3】:左下角的数一定比右上角的数大,选定一个数作为比较基准,然后和target做匹配,往不同的方向切

1.题目内容

给你一个满足下述两条属性的 m x n 整数矩阵:

  • 每行中的整数从左到右按非严格递增顺序排列。
  • 每行的第一个整数大于前一行的最后一个整数。

给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false

image-20241101134602169

2.题解思路

👻方法1:一次二分法(分行、分列检索:先定位target所在行,然后按行检索)

  • 思路分析:遍历每一行元素,然后对每一行执行二分检索(对比方法2的思路,这种思路比较纯粹,不用纠结这个rowIdx应该怎么取值以及考虑奇奇怪怪的数组边界问题)
public boolean searchMatrix(int[][] matrix, int target) {
    for (int i = 0; i < matrix.length; i++) {
        // 校验每行的二分检索结果
        int findRow = binarySearch(matrix[i],target);
        // 二分检索成功返回true
        if (findRow != -1) {
            return true;
        }
    }
    return false;
}

/**
 * 定义二分查找方法(针对一维数组)
 */
public int binarySearch(int[] row, int target) {
    int left = 0, right = row.length - 1;
    while (left <= right) {
        // 定义中点位置
        int mid = left + (right - left) / 2;
        // 与target进行比较
        if(row[mid] == target) {
            return mid;
        }else if(target < row[mid]) {
            right = mid - 1;
        }else if(target > row[mid]) {
            left = mid + 1;
        }
    }
    // 无匹配元素返回-1(如果是要返回下一个插入位置则return left或者right+1)
    return -1;
}

👻方法2:二分法(分行、分列检索:先定位target所在行,然后按行检索)

  • 思路分析:分析矩阵数据存储规律,按行、按列递增,因此可以先锁定元素在哪一行或者哪一列,然后再遍历对应行列进行检索
    • 例如先遍历第1列,确定元素在哪一行位置,然后再按行检索(转为二分检索)
      • 遍历第1列,如果target存在则直接返回,如果大于或小于则缩边
      • 拿到target对应行,则可按行遍历target(普通的一维数组二分检索)
    • 无论是按行还是按列都可以用二分法实现,但是需要注意的是边界的处理(需要调整二分检索的实现)
/**
 * 搜索二维矩阵(74)
 */
public class Solution3 {
    /**
     * 二分法:矩阵元素按行或者按列是递增的,因此可以先定位行,再检索行
     */
    public boolean searchMatrix(int[][] matrix, int target) {
        // 先定位target位于哪一行
        int rowIdx = searchByColumn(matrix, target);
        if (rowIdx < 0) {
            return false; // 记录不在指定行
        }
        return searchByRow(matrix[rowIdx], target);
    }

    // 检索目标在哪一行(根据第1列进行检索)
    private int searchByColumn(int[][] matrix, int target) {
        int low = -1, high = matrix.length - 1;
        while (low < high) {
            int mid = (high - low + 1) / 2 + low;
            if (matrix[mid][0] <= target) {
                low = mid;
            } else {
                high = mid - 1;
            }
        }
        return low;
    }

    // 检索目标在哪一行
    private boolean searchByRow(int[] row, int target) {
        int left = 0, right = row.length - 1;
        // 如果继续走到此处,拿到top,表示定位到target对应行
        while (left <= right) {
            // int mid = (left + right) / 2;
            int mid = left + (right - left) / 2;
            // 判断target和matrix[top][mid]的大小进行缩圈
            if (target == row[mid]) {
                return true;
            }
            if (target < row[mid]) {
                // target在左半部分
                right = mid - 1;
            } else if (target > row[mid]) {
                // target在右半部分
                left = mid + 1;
            }
        }
        return false;
    }
}
  • 复杂度分析

    • 时间复杂度:O(logm + logn)=O(logmn)m、n分别是矩阵的行和列

    • 空间复杂度:O(1)

方法3:规律法(从左下角开始找)

  • 思路分析:元素按行、按列是有序递增的。左下角的数总是比右上角的数要大,因此可以选择左下角或者右上角作为判断base([row,col]),根据和target进行比较决定指针的移动位置
    • 如果base == target 返回 true
    • 如果base > target :说明此时 base 比较大,要让base变小才可能找到target,因此row--
    • 如果base < target :说明此时 base 比较小,要让base变大才可能找到target,因此col++
    • 当row、col 指针走到数组边界则查找结束
/**
 * 搜索二维矩阵(74)
 */
public class Solution4 {
    /**
     * 规律法:选择左下角开始作为比较基准base(row,col),和target进行匹配
     * - 如果base>target: 说明要往上走才能找到更小的值,因此row--
     * - 如果base<target:说明要往右走才能找到更大的值,因此col++
     * - base==target 找到目标值
     */
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        // 初始化从左下角的元素开始比较
        int row = m - 1, col = 0;
        // 当两个指针有一个走到边界则结束
        while (row >= 0 && col <= n - 1) {
            int base = matrix[row][col];
            if (base > target) {
                row--;
            } else if (base < target) {
                col++;
            } else if (base == target) {
                return true;
            }
        }
        // 没找到
        return false;
    }
}

👻方法4:暴力法(常规检索二维矩阵)❌

/**
 * 搜索二维矩阵(74)
 */
public class Solution1 {
    /**
     * 判断元素是否存在于矩阵中
     */
    public boolean searchMatrix(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        for (int i = 0; i < m ; i++) {
            for (int j = 0; j < n ; j++) {
                if (matrix[i][j] == target) {
                    return true;
                }
            }
        }
        /*
         for (int[] ints : matrix) {
            for (int anInt : ints) {
                if (anInt == target) return true;
            }
        }
         */
        return false;
    }
}
  • 复杂度分析

    • 时间复杂度:O(n2

    • 空间复杂度:O(1)

🟡03-寻找峰值(162)

1.题目内容

峰值元素是指其值严格大于左右相邻值的元素。

给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞

你必须实现时间复杂度为 O(log n) 的算法来解决此问题。

示例 1:

输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。

示例 2:

输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5 
解释:你的函数可以返回索引 1,其峰值元素为 2;
     或者返回索引 5, 其峰值元素为 6。

2.题解思路

👻方法1:遍历

  • 思路分析:中规中矩循环遍历数组元素,每次比较3个元素,看中间的元素是否满足峰值条件,但需注意数组边界的条件处理
    • 数组边界:
      • 如果只有1个元素,其本身就是峰值
      • 如果有两个元素,则返回元素较大的那个元素的索引(元素较大为峰值)
      • 判断首尾两个元素,如果首元素大于下一个元素则返回0(首元素索引),如果尾元素大于前一个元素则返回len-1 (尾元素索引)
public int findPeakElement(int[] nums) {
    int len = nums.length;
    // 临界条件判断
    // 只有1个元素,元素本身就是峰值
    if (len == 0 || len == 1) {
        return 0;
    }
    // 有2个元素,返回比较大的元素索引
    if (len == 2) {
        return nums[0] < nums[1] ? 1 : 0;
    }
    // 首、尾节点判断
    if (nums[0] > nums[1]) {
        return 0;
    }
    if (nums[len - 1] > nums[len - 2]) {
        return len - 1;
    }

    // 其他情况:循环遍历检索
    for (int i = 0; i < len - 2; i++) {
        int first = nums[i];
        int second = nums[i + 1];
        int third = nums[i + 2];
        // 判断当前序列是否为峰值
        if (second > first && second > third) {
            return i + 1;
        }
    }
    return 0;
}
  • 复杂度分析

    • 时间复杂度:O(n)

    • 空间复杂度:O(1)

👻方法2:寻找最大值

​ 结合题意分析,由于题目确保了nums[i]不等于 nums[i+1],则可以确保的是对于最大值来说,最大值两侧的元素一定严格小于最大值本身,因此可以将题目转化为寻找最大值,即最大值所在的位置就是一个可行的峰值位置

/**
 * 162 寻找峰值
 */
public class Solution2 {
    public int findPeakElement(int[] nums) {
        // 定义当前最大值的索引,初始化为0
        int maxIdx = 0;
        for(int i=0;i<nums.length;i++){
            // 如果当前遍历元素值大于目前的最大值元素,则进行最大值索引替换
            if(nums[i] > nums[maxIdx]){
                maxIdx = i;
            }
        }
        return maxIdx;
    }
}
  • 复杂度分析

    • 时间复杂度:O(n)

    • 空间复杂度:O(1)

👻方法3:贪心二分法(上升必有峰)

  • 由于题目限定nums[i]不等于 nums[i+1],因此只要存在峰值则必然是上升,因此可以通过二分法锁定上升序列
    • 比较nums[mid] 与 nums[mid + 1]
      • 如果nums[mid] < nums[mid + 1] 说明右边区间存在上升序列,左侧缩边到mid+1
      • 如果nums[mid] > nums[mid + 1] 说明左侧区间可能存在上升序列,右侧缩边到mid(就算此时右区间可能存在峰值被丢掉也没关系,因为起码缩圈能够保证至少有1个)
    • 最终返回left
/**
 * 162 寻找峰值
 */
public class Solution3 {
    /**
     * 思路:二分(O(logN)二分法的代名词)
     */
    public int findPeakElement(int[] nums) {
        int left = 0, right = nums.length - 1;
        while (left < right) {
            int mid = (left + right) / 2;
            // 中间判断
            if (nums[mid] < nums[mid + 1]) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left;
    }
}

复杂度分析

  • 时间复杂度:O(log n)

  • 空间复杂度:O(1)

🟡04-搜索旋转排序数组(33)

1.题目内容

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

示例 2:

输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1

示例 3:

输入:nums = [1], target = 0
输出:-1

2.题解思路

思路分析

【思路1】中规中矩,二分法的思路是对有序数组进行操作,题目中数组原本有序,但是由于旋转规则未知,因此无法直接用二分法,最硬核的就是先排序,后二分,或者直接遍历检索,但是这种方式显然不满足要求

【思路2】基于思路1中引申的问题进一步思考,可以看到经过旋转后的数组是局部有序的,通过这个轴点将数组划分为升序的两部分。因此可以将这个题目转化为寻找这个轴点,如果轴点存在则说明出现了旋转(根据轴点拆分为两个数组,然后分别进行二分),如果没有出现轴点则说明数组没有旋转,则直接可以对整个数组进行二分

image-20241101211942739

👻方法1:找轴点+分段二分

  • 思路分析
    • 找轴点
      • 数组本身有序,如果经过旋转后则必然会存在突然断序的情况(即连续升序,断开的那个点就是轴点
      • 数组为空、数组长度为0或者1都视作无旋转
    • 分段二分
      • 如果轴点不存在,则说明数组没有旋转,直接对整个数组进行二分查找
      • 如果轴点存在,则说明数组通过轴点旋转,则对数组进行切割然后分别二分获取结果
/**
 * 33 搜索旋转数组
 */
public class Solution1 {

    /**
     * 思路:找轴点+分段二分
     */
    public int search(int[] nums, int target) {

        int findPoint = getPoint(nums);
        if (findPoint == -1) {
            // 不存在轴点(无旋转),可以直接对整个数组进行二分
            return binarySearch(nums, target);
        }

        // 存在轴点的情况处理(分段二分)
        int[] leftNums = Arrays.copyOfRange(nums, 0, findPoint + 1);
        int[] rightNums = Arrays.copyOfRange(nums, findPoint + 1, nums.length);
        int leftSearch = binarySearch(leftNums, target);
        int rightSearch = binarySearch(rightNums, target);
        // 校验结果(数据只有存在与左侧或右侧)
        // 如果左侧结果存在则返回
        if (leftSearch != -1) {
            return leftSearch;
        }
        // 如果左侧结果不存在则返回右侧结果
        if (rightSearch != -1) {
            return rightSearch + leftNums.length;
        }
        return -1;
    }


    /**
     * 寻找轴点:判断数组是否旋转
     * 如果未旋转(完全升序):返回-1(表示轴点不存在)
     * 如果存在旋转(存在降序):返回轴点索引位置
     */
    public int getPoint(int[] nums) {
        // 判断数组边界
        if (nums == null || nums.length == 0 || nums.length == 1) {
            return -1;
        }
        // 判断是否完全升序
        for (int i = 0; i < nums.length - 1; i++) { // 注意数组边界处理
            if (nums[i] > nums[i + 1]) {
                // 出现降序,则轴点位置存在
                return i;
            }
        }
        return -1;
    }

    // 二分法检索
    public int binarySearch(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            // 判断target是否匹配
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            }
        }
        // 结果不存在
        return -1; // return left 表示下一个可插入的位置
    }

}
  • 复杂度分析

    • 时间复杂度:O(logN)

    • 空间复杂度:O(n)如果数组被旋转,则需要借助辅助的数组来分段二分

算法优化:基于原数组进行检索(不使用copyOfRange

​ 当确认了旋转基点之后,此处不需要单独copy数组进行切分,而是在原数组基础上直接限定范围进行二分检索(确定检索的起止点)即可

/**
 * 🟡 033 搜索旋转排序数组 - https://leetcode.cn/problems/search-in-rotated-sorted-array/description/
 */
public class Solution033_01 {

    /**
     * 思路:寻转旋转基点 + 二分检索,区分有旋转和无旋转两种情况分析
     */
    public int search(int[] nums, int target) {
        int findRotateIdx = searchRotateIdx(nums);
        if (findRotateIdx == -1) {
            // 无旋转,直接调用二分法检索
            return binarySearch(nums, 0, nums.length - 1, target);
        } else {
            // 有旋转,拆分数组进行二分法检索
            int find1 = binarySearch(nums, 0, findRotateIdx - 1, target);
            int find2 = binarySearch(nums, findRotateIdx, nums.length - 1, target);
            return find1 != -1 ? find1 : find2;
        }
    }

    // 寻找旋转基点
    private int searchRotateIdx(int[] nums) {
        int idx = -1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] <= nums[i - 1]) {
                idx = i;
                break; // 出现骤降点
            }
        }
        return idx;
    }

    /**
     * 二分检索(针对指定检索范围)
     */
    private int binarySearch(int[] nums, int start, int end, int target) {
        int left = start, right = end;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (target < nums[mid]) {
                right = mid - 1;
            } else if (target > nums[mid]) {
                left = mid + 1;
            }
        }
        return -1; // 未找到指定target
    }

}

🟡05-在排序数组中查找元素的第1个和最后1个位置(34)🚀

1.题目内容

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

2.题解思路

👻方法1:low_bound 概念🚀

思路分析

核心思路:掌握二分查找的三种方式的任意一种,然后将二分查找进行分类>=><=<,转化成>x的进行求解

​ 设定任意一种二分查找的方式(闭区间、左闭右开区间、开区间),将有序整数数组上的二分查找分为4种类型,分别是>=><=<,而这四种类型是可以相互转化的

  • 例如查找>x的位置,实际上可以等价于查找>= x+1 的位置

  • 类似地如果是<x的位置,则可以等价于查找(>=x) -1即大于等于x的的那个数的左边的数

  • 类似地如果是<=x的位置,则可以等价于查找(>x) -1即大于等于x的的那个数的左边的数

​ 思路分析:题目中要求的是目标值在数组中的开始位置和结束位置>=x<=x的位置

// 闭区间
public int low_bound (int[] nums, int target) {
    // 定义检索区间
    int left = 0, right = nums.length - 1; // 闭区间
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (target < nums[mid]) {
            // 如果目标在左侧,则右侧缩边
            right = mid - 1;
        } else if (target > nums[mid]) {
            // 如果目标在右侧,则左侧缩边
            left = mid + 1;
        } else {
            return mid;
        }
    }
    // 返回元素可插入的下个位置(如果不存在可以直接返回-1)
    return left; // return -1;
}
/**
     * 检索法: 二分检索
     * 转化为查找>=x的位置和<=x(( > x) - 1)的位置
     */

public int[] searchRange(int[] nums, int target) {
    int startIdx = -1,endIdx = -1;
    // 先检索第1个满足的target索引
    startIdx = lowBound(nums,target);
    if (startIdx == nums.length || nums[startIdx]!=target){
        // 元素不存在
        return new int[]{-1,-1};
    }
    // 如果firstIdx存在,则继续寻找endIdx的值
    endIdx = lowBound(nums,target + 1) - 1; // 寻找到>x的位置,定位到其前一个数
    return new int[]{startIdx,endIdx};
}


public int lowBound(int[] nums, int target) {
    // 定义检索区间
    int left = 0, right = nums.length - 1; // 闭区间
    while (left <= right) { // 区间不为空时
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1; // 取[mid,+1,right]区间
        } else {
            right = mid - 1; // 取[left,mid-1]区间 (target==nums[mid]的情况也包括在内)
        }
    }
    // 返回元素可插入的下个位置(如果不存在可以直接返回-1)
    return left; // return -1;
}
  • 复杂度分析

    • 时间复杂度:O(log n)

    • 空间复杂度:O(1)常数级别的应用

👻方法2:暴力法

核心:循环遍历数组(数组本身升序排列),定义一个数组存储结果集合,这个结果集可以理解为(第一个匹配的元素索引,第一个匹配的元素索引+匹配个数(作为结束索引))

// 暴力法:循环遍历数组,一次进行校验
public int[] searchRange(int[] nums, int target) {
    // 定义结果集(开始索引,结束索引)
    int[] res = {-1,-1};
    // 定义target在数组中的匹配个数(计数器)
    int count = 0;
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] == target) {
            if(count == 0){
                // 表示第一个检索到的匹配元素,将其加入结果集
                res[0] = i;
            }
            count++;
        }
    }
    // 最终封装结果集并返回
    if(res[0] != -1){
        res[1] = res[0] + count -1;
    }
    return res;
}

👻方法3:正向二分+逆向二分

  • 思路分析:正向二分找左边界 + 逆向二分找右边界

  • 复杂度分析

    • 时间复杂度:

    • 空间复杂度:

🟡06-寻找旋转排序数组中的最小值(153)

1.题目内容

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

2.题解思路

👻方法1:二分法

  • 思路分析:旋转后的数组一定会被划分为前后两部分的升序数组,且前面部分的最小值一定大于后一半的最大值,因此只要通过二分法找到后半部分的第1个元素即可。采用二分查找思路,然后不断缩小区间取值
/**
 * 153 寻找旋转排序数组的中的最小值
 */
public class Solution1 {

    /**
     * 遍历法
     */
    public int findMin(int[] nums) {
        // 初始化最小值
        int min = nums[0];

        // 闭区间思路
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < min) {
                // 如果mid所在位置元素小于min,则其左侧可能还会存在比min更小的数,因此要更新min的值,右侧缩边
                min = nums[mid];
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }

        // 返回最小值
        return min;
    }

}
  • 复杂度分析

    • 时间复杂度:O(log n)二分检索

    • 空间复杂度:O(1)常数级别的空间占用

👻方法2:规律法

  • 思路分析:结合题意分析,未知数组是否旋转,如果数组出现旋转则这个排序数组必然会出现突然断序的情况,则这个断续的位置就是最小值所在位置,因此可以通过遍历继续数组元素进行判断。如果数组元素连续升序则说明无旋转(最小值就是第1个),如果数组元素遍历过程中出现断序的情况,则断续位置就是最小值
public class Solution1 {
    /**
     * 遍历法
     */
    public int findMin(int[] nums) {
        int idx = validRotate(nums);
        return idx == -1 ? nums[0] : nums[idx];
    }

    public int validRotate(int[] nums) {
        int idx = -1; // 如果无旋转则返回-1
        // 如果数组为空或者数组长度为1则视作没有旋转
        if (nums == null || nums.length == 0 || nums.length == 1) {
            return -1;
        }
        // 判断数组是否连续升序
        for (int i = 0; i < nums.length - 1; i++) {
            if (nums[i] > nums[i + 1]) { // 连续升序出现断序
                idx = i + 1; // 此处取i+1表示这个位置是最小的数
            }
        }
        return idx;
    }
}
  • 复杂度分析

    • 时间复杂度:O(n)最差的情况需要遍历整个数组,n为数组长度

    • 空间复杂度:O(1)只需要常数级的空间占用

🔴07-寻找两个正序数组的中位数(04)

1.题目内容

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

算法的时间复杂度应该为 O(log (m+n))

示例 1:

输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

示例 2:

输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

2.题解思路

👻方法1:合并排序数组 + 奇偶找中位(❌非O(logn))

/**
 * 004 寻找两个正序数组的中位数
 */
public class Solution1 {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int[] mergeNums = merge(nums1, nums2);

        int len = mergeNums.length;
        if (len % 2 == 0) {
            // 偶数
            int left = 0, right = len - 1;
            int mid = left + (right - left) / 2;
            return ((double) mergeNums[mid] + (double)mergeNums[mid+1]) / 2; // 需将int转为double做除法
        } else {
            // 奇数
            return mergeNums[mergeNums.length / 2];
        }
    }

    // 合并两个有序数组
    public int[] merge(int[] nums1, int[] nums2) {
        int len1 = nums1.length, len2 = nums2.length;
        // 定义数组存储合并后的内容
        int[] nums = new int[len1 + len2];
        // 定义数组指针,分别装箱nums、nums1、nums2当前遍历位置
        int cur = 0, p1 = 0, p2 = 0;
        while (p1 < len1 && p2 < len2) {
            // 如果p1指向元素较小则加入nums1[p1]
            if (nums1[p1] < nums2[p2]) {
                nums[cur] = nums1[p1];
                // 指针后移
                cur++;
                p1++;
            } else {
                nums[cur] = nums2[p2];
                // 指针后移
                cur++;
                p2++;
            }
        }

        // 判断是否有多余长度的元素需补充
        while (p1 < len1) {
            nums[cur] = nums1[p1];
            cur++;
            p1++;
        }
        while (p2 < len2) {
            nums[cur] = nums2[p2];
            cur++;
            p2++;
        }

        // 返回合并后的有序数组
        return nums;
    }
}
  • 复杂度分析

    • 时间复杂度:O(m+n) 遍历所有元素进行合并
  • 空间复杂度:O(m+n)需m+n空间存储合并后的排序数组

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3