力扣刷题笔记

题目来源:

  • 力扣:力扣官网,题目和解题思路著作权归领扣网络所有。

刷题

力扣第33题、搜索旋转排序数组

假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -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

官方思路:二分搜索

  • 题目要求算法时间复杂度必须是 O(log n) 的级别,这提示我们可以使用二分搜索的方法。

  • 但是数组本身不是有序的,进行旋转后只保证了数组的局部是有序的,这还能进行二分搜索吗?答案是可以的。

  • 可以发现的是,我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。拿示例来看,我们从 6 这个位置分开以后数组变成了 [4, 5, 6] 和 [7, 0, 1, 2] 两个部分,其中左边 [4, 5, 6] 这个部分的数组是有序的,其他也是如此。

  • 这启示我们可以在常规二分搜索的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分搜索的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:

    如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [\textit{nums}[l],\textit{nums}[mid])[nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。

    如果 [mid, r] 是有序数组,且 target 的大小满足 (\textit{nums}[mid+1],\textit{nums}[r]](nums[mid+1],nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。

  • 二分搜索、两个判断范围的条件:是否有序,有序范围是否包含目标

代码

class Solution {
    public int search(int[] nums, int target) {
        if(nums.length == 0)
            return -1;
        if(nums.length == 1)
            return nums[0] == target ? 0 : -1 ;
        int l=0,r=nums.length-1;
        while(l<=r){
            int mid = (l+r)/2;
            if(nums[mid] == target)
                return mid;
            if(nums[0]<= nums[mid]) {
                if(nums[0]<= target && target <= nums[mid]) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            } else {
                if(nums[mid]<= target && target <= nums[nums.length - 1]) {
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
        }
        return -1;
    }
}
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = (int)nums.size();
        if (!n) return -1;
        if (n == 1) return nums[0] == target ? 0 : -1;
        int l = 0, r = n - 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] == target) return mid;
            if (nums[0] <= nums[mid]) {
                if (nums[0] <= target && target < nums[mid]) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            } else {
                if (nums[mid] < target && target <= nums[n - 1]) {
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
        }
        return -1;
    }
};

力扣[面试题56 - I]、数组中数字出现次数

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

  • 示例 1:
    输入:nums = [4,1,4,6]
    输出:[1,6] 或 [6,1]

  • 示例 2:
    输入:nums = [1,2,10,4,1,4,3,3]
    输出:[2,10] 或 [10,2]

  • 限制:
    2 <= nums <= 10000

官方思路:分组 位运算

  • 让我们先来考虑一个比较简单的问题:

如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字?

  • 答案很简单:全员进行异或操作即可。考虑异或操作的性质:对于两个操作数的每一位,相同结果为 00,不同结果为 11。那么在计算过程中,成对出现的数字的所有位会两两抵消为 00,最终得到的结果就是那个出现了一次的数字。

  • 那么这一方法如何扩展到找出两个出现一次的数字呢?
    如果我们可以把所有数字分成两组,使得:
    1、两个只出现一次的数字在不同的组中;
    2、相同的数字会被分到相同的组中。
    那么对两个组分别进行异或操作,即可得到答案的两个数字。这是解决这个问题的关键。

  • 那么如何实现这样的分组呢?
    解题关键思路

代码

先对所有数字进行一次异或,得到两个出现一次的数字的异或值。
在异或结果中找到任意为 11 的位。
根据这一位对所有的数字进行分组。
在每个组内进行异或操作,得到两个数字。

class Solution {
public:
    vector<int> singleNumbers(vector<int>& nums) {
        int ret = 0;
        for (int n : nums)
            ret ^= n;
        int div = 1;
        while ((div & ret) == 0)
            div <<= 1;
        int a = 0, b = 0;
        for (int n : nums)
            if (div & n)
                a ^= n;
            else
                b ^= n;
        return vector<int>{a, b};
    }
};
class Solution {
    public int[] singleNumbers(int[] nums) {
        int ret = 0;
     
        for (int n : nums) {
            ret ^= n;
        }
        int div = 1;
        while ((div & ret) == 0)
            div <<= 1;
        int a = 0, b = 0;
        for (int n : nums)
            if ((div & n)==0)
                a ^= n;
            else
                b ^= n;
        int ab[] = new int[2];;
        ab[0]=a;
        ab[1]=b;
        return ab;
    }
}

力扣第11题、盛最多水的容器

给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。

图示

  • 示例 1

输入:[1,8,6,2,5,4,8,3,7]
输出:49

官方思路:双指针

  • 在初始时,左右指针分别指向数组的左右两端,计算出容纳的水量。
  • 此时我们需要移动一个指针。移动哪一个呢?直觉告诉我们,应该移动对应数字较小的那个指针。这是因为,由于容纳的水量是由两个指针指向的数字中较小值 * 指针之间的距离决定的。如果我们移动数字较大的那个指针,那么前者「两个指针指向的数字中较小值」不会增加,后者「指针之间的距离」会减小,那么这个乘积会减小。因此,我们移动数字较大的那个指针是不合理的。因此,我们移动 数字较小的那个指针。
  • 反复选择并移动指针,直到两个指针位置重合,比较得到每次移动时容纳的水量中的最大值。
  • 证明:为什么双指针的做法是正确的?

双指针代表了什么?

双指针代表的是可以作为容器边界的所有位置的范围。在一开始,双指针指向数组的左右边界,表示数组中所有的位置都可以作为容器的边界,因为我们还没有进行过任何尝试。在这之后,我们每次将对应的数字较小的那个指针另一个指针的方向移动一个位置,就表示我们认为这个指针不可能再作为容器的边界了

为什么对应的数字较小的那个指针不可能再作为容器的边界了?

考虑第一步,用示例来说,假设当前左指针和右指针指向的数分别为 xy,不失一般性,我们假设x≤y。同时,两个指针之间的距离为t。那么,它们组成的容器的容量为:min(x,y)∗t=x∗t
数学表达式

  • 即无论我们怎么移动右指针,得到的容器的容量都小于移动前容器的容量。也就是说,这个左指针对应的数不会作为容器的边界了,那么我们就可以丢弃这个位置,将左指针向右移动一个位置,此时新的左指针于原先的右指针之间的左右位置,才可能会作为容器的边界。
  • 这样一来,我们将问题的规模减小了 1,被我们丢弃的那个位置就相当于消失了。此时的左右指针,就指向了一个新的、规模减少了的问题的数组的左右边界,因此,我们可以继续像之前 考虑第一步 那样考虑这个问题:
  • 求出当前双指针对应的容器的容量;
  • 对应数字较小的那个指针以后不可能作为容器的边界了,将其丢弃,并移动对应的指针。

代码

public class Solution {
    public int maxArea(int[] height) {
        int l = 0, r = height.length - 1;
        int ans = 0;
        while (l < r) {
            int area = Math.min(height[l], height[r]) * (r - l);
            ans = Math.max(ans, area);
            if (height[l] <= height[r]) {
                ++l;
            }
            else {
                --r;
            }
        }
        return ans;
    }
}

力扣第1095题、山脉数组中查找目标值

  • 这是一个 交互式问题 (交互式问题即可以调用题目给出的结果,通过调用接口完成题目)。

  • 给你一个 山脉数组 mountainArr,请你返回能够使得 mountainArr.get(index) 等于 target 最小 的下标 index 值。如果不存在这样的下标 index,就请返回 -1。

  • 何为山脉数组?如果数组 A 是一个山脉数组的话,那它满足如下条件:
    在这里插入图片描述

  • 注意:对 MountainArray.get 发起超过 100 次调用的提交将被视为错误答案。此外,任何试图规避判题系统的解决方案都将会导致比赛资格被取消。

  • 示例 1:
    输入:array = [1,2,3,4,5,3,1], target = 3
    输出:2
    解释:3 在数组中出现了两次,下标分别为 2 和 5,我们返回最小的下标 2。

  • 示例 2:
    输入:array = [0,1,2,4,2,1], target = 3
    输出:-1
    解释:3 在数组中没有出现,返回 -1。

官方思路:二分查找

  • 显然,如果山脉数组是一个单调递增或者单调递减的序列,那么我们可以通过二分法迅速找到目标值。
  • 而现在题目中有一个单调递增序列(峰值左边)和一个单调递减序列(峰值右边),我们只是不知道两个序列的分割点,即峰值在哪里。所以我们第一步应该首先找到峰值。
  • 而峰值也可以使用二分法寻找:
    在这里插入图片描述
  • 以示例 1 为例,我们对整个数组进行差分,即除了第一个数每个数都减去前一个数得到新的数组,最终我们得到 [1, 1, 1, 1, -2, -2],整个差分数组满足单调性,可以应用二分法。
  • 接下来我们只需要使用二分法在单调序列中找到目标值即可,注意二分法要使用两次,为了编码简洁可以将二分法封装成函数。

代码

  • 先使用二分法找到数组的峰值。
  • 在峰值左边使用二分法寻找目标值。
  • 如果峰值左边没有目标值,那么使用二分法在峰值右边寻找目标值。
/**
 * // This is MountainArray's API interface.
 * // You should not implement it, or speculate about its implementation
 * interface MountainArray {
 *     public int get(int index) {}
 *     public int length() {}
 * }
 */
 
class Solution {
    public int findInMountainArray(int target, MountainArray mountainArr) {
        int len = mountainArr.length();

        int peakIndex = findMountainTop(mountainArr, 0, len-1);
        if(mountainArr.get(peakIndex) == target)
            return peakIndex;
        
        int res = findSortedArray(mountainArr, 0, peakIndex - 1, target);
        if(res != -1)
            return res;
        
        return findReverseArray(mountainArr, peakIndex + 1, len - 1, target);
    }

    /*
     * 在[left...right] 查找山顶元素的下标 
     */
     private int findMountainTop(MountainArray mountainArr, int left, int right) {
        while(left < right) {
            int mid = left + (right - left) / 2;
            if(mountainArr.get(mid) < mountainArr.get(mid +1)) {
                // 下一轮搜索区间 [mid + 1, right]
                left = mid + 1;
            } else {
                // 下一轮搜索区间
                right = mid;
            }
        }
        // left==right
        return left;
     }


     /*
      * 在左侧查找target的下标
      */
    private int findSortedArray(MountainArray mountainArr, int left, int right, int target) {
        while(left < right) {
            int mid = left + (right - left) / 2;
            if(mountainArr.get(mid) < target) {
                // 下一轮搜索区间 [mid + 1, right]
                left = mid + 1;
            } else {
                // 下一轮搜索区间
                right = mid;
            }
        }      
        if (mountainArr.get(left) == target){
            return left;
        }
        return -1;  
    }

    /*
     * 在右侧查找target的下标
     */
    private int findReverseArray(MountainArray mountainArr, int left, int right, int target) {
         while(left < right) {
            int mid = left + (right - left + 1) / 2;
            if(mountainArr.get(mid) < target) {
                // 下一轮搜索区间 [left,  mid -1]
                right = mid - 1;
            } else {
                // 下一轮搜索区间[mid, right]
                // [left, right(mid)]
                left = mid ;
            }
        }   
        if (mountainArr.get(left) == target){
            return left;
        }
        return -1;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值