剑指 数组03、04、29、53

03、数组中重复数字

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

输入: [2, 3, 1, 0, 2, 5, 3] 输出:2 或 3
限制:2 <= n <= 100000

方法一:先排序,然后看相邻元素是否有相同的,有直接return。不过很慢,时间O(nlogn)了,空间O(1)

class Solution {
    public int findRepeatNumber(int[] nums) {
        Arrays.sort(nums);
        for (int i = 1; i < nums.length; i++) 
        {
            if (nums[i] == nums[i -1])
                return nums[i];
        }
        return -1;
    }
}

方法二:哈希表 时间O(n),空间O(n)

  int n = nums.length;
  Set<Integer> set = new HashSet<Integer>();
        
  for (int i = 0; i < n; i++) {
  if (set.contains(nums[i])) 
       return nums[i];
  set.add(nums[i]);

方法三:时间复杂度O(n),空间复杂度O(1)。可以看做是一种原地哈希,不过没有用到字典。具体做法就是因为题目中给的元素是 < len(nums)的,所以我们可以让 位置i 的地方放元素i。如果位置i的元素不是i的话,那么我们就把i元素的位置放到它应该在的位置,即 nums[i] 和nums[nums[i]]的元素交换,这样就把原来在nums[i]的元素正确归位了。如果发现 要把 nums[i]正确归位的时候,发现nums[i](这个nums[i]是下标)那个位置上的元素和要归位的元素已经一样了,说明就重复了,重复了就return。

class Solution {
    public int findRepeatNumber(int[] nums) {
        int temp;
        for(int i=0;i<nums.length;i++){
            while (nums[i]!=i){
                if(nums[i]==nums[nums[i]]){
                    return nums[i];
                }
                temp=nums[i];
                nums[i]=nums[temp];
                nums[temp]=temp;
            }
        }
        return -1;
    }
}

方法四:(不修改数组找出重复数字)

04、二维数组中的查找

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
给定 target = 5,返回 true。
给定 target = 20,返回 false。
限制:0 <= n <= 1000
0 <= m <= 1000

方法一:逐行使用二分查找
思路分析:
要查找矩阵中是否存在某个元素,并且表示矩阵每一行的数组都是有序的,可以对每一行进行二分查找,如果每一行都没有查找到结果,就返回false。
使用java库函数Arrays.binarySearch(matrix[i], target),返回值非负说明查找到target。

还需要处理一些特殊情况:
给定的矩阵为null或者给定的矩阵不存在任何元素matrix.length == 0 || matrix[0].length == 0,肯定找不到目标元素,直接返回。
矩阵的每一列也是从小到达排列的,所以在对每一行进行二分查找的循环是,如果matrix[i][0] > target,这一行肯定没有指定元素,更下面的每一行的所有元素都大于matrix[i][0]也一定找不到指定元素,所以可以直接返回false。

时间复杂度为O(mlog(n)),空间复杂度为O(1)。

public boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
            return false;
        for (int i = 0; i < matrix.length; i++) {
            if (matrix[i][0] > target)
                return false;
            if (Arrays.binarySearch(matrix[i], target) >= 0)
                return true;
        }
        return false;
    }

方法二:线性查找(类似于Binary Search Tree 二叉搜索树,节点左子树都小于该节点,右子树都大于该节点)
时间复杂度:O(n+m)。访问到的下标的行最多增加 n 次,列最多减少 m 次,因此循环体最多执行 n + m 次。
空间复杂度:O(1)。

如果当前元素大于目标值,说明当前元素的下边的所有元素都一定大于目标值,因此往下查找不可能找到目标值,往左查找可能找到目标值。如果当前元素小于目标值,说明当前元素的左边的所有元素都一定小于目标值,因此往左查找不可能找到目标值,往下查找可能找到目标值。

class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) 
        {
            return false;
        }
        int rows = matrix.length, columns = matrix[0].length;
        int row = 0, column = columns - 1;
        while (row < rows && column >= 0)
         {
            int num = matrix[row][column];
            if (num == target)
               {return true;} 
            else if (num > target) 
              {column--; } 
            else
              { row++; }
        }
        return false;
   }

29、顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。

示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
限制:
0 <= matrix.length <= 100
0 <= matrix[i].length <= 100

方法一:设定边界
算法流程:
空值处理: 当 matrix 为空时,直接返回空列表 [] 即可。
初始化: 矩阵 左、右、上、下 四个边界 l , r , t , b ,用于打印的结果列表 res 。
循环打印: “从左向右、从上向下、从右向左、从下向上” 四个方向循环,每个方向打印中做以下三件事 (各方向的具体信息见下表)
根据边界打印,即将元素按顺序添加至列表 res 尾部;
边界向内收缩 1 (代表已被打印);
判断是否打印完毕(边界是否相遇),若打印完毕则跳出

打印方向1. 根据边界打印2. 边界向内收缩3. 是否打印完毕
从左向右左边界l,右边界 r上边界 t 加 1是否 t > b
从上向下上边界 t ,下边界b右边界 r 减 1是否l > r
从右向左右边界 r ,左边界l下边界 b 减 1是否 t > b
从下向上下边界 b ,上边界t左边界 l 加 1是否 l > r

复杂度分析:
时间复杂度 O(MN): M, N 分别为矩阵行数和列数。
空间复杂度 O(1): 四个边界 l , r , t , b 使用常数大小的 额外 空间( res 为必须使用的空间)。

class Solution {
    public int[] spiralOrder(int[][] matrix) {
        if(matrix.length == 0) return new int[0];
        int l = 0, r = matrix[0].length - 1, t = 0, b = matrix.length - 1, x = 0;
        int[] res = new int[(r + 1) * (b + 1)];
        while(true) {
            for(int i = l; i <= r; i++) res[x++] = matrix[t][i]; // left to right.
            if(++t > b) break;
            for(int i = t; i <= b; i++) res[x++] = matrix[i][r]; // top to bottom.
            if(l > --r) break;
            for(int i = r; i >= l; i--) res[x++] = matrix[b][i]; // right to left.
            if(t > --b) break;
            for(int i = b; i >= t; i--) res[x++] = matrix[i][l]; // bottom to top.
            if(++l > r) break;
        }
        return res;
    }
}

方法二:按层模拟

53-1、在排序数组中查找数字

统计一个数字在排序数组中出现的次数。

示例 1:输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:输入: nums = [5,7,7,8,8,10], target = 6
输出: 0
限制:0 <= 数组长度 <= 50000

方法一:算法解析:
初始化: 左边界 i = 0 ,右边界 j = len(nums) - 1。
循环二分: 当闭区间 [i, j]无元素时跳出;
计算中点 m = (i + j) / 2 (向下取整);
若 nums[m] < target ,则 target在闭区间 [m + 1, j] 中,因此执行 i = m + 1;
若 nums[m] > target ,则 target 在闭区间 [i, m - 1] 中,因此执行 j = m - 1;
若 nums[m] = targetn ,则右边界 right 在闭区间 [m+1, j]中;左边界 left在闭区间 [i, m-1] 中。因此分为以下两种情况:
若查找 右边界 right ,则执行 i = m + 1;(跳出时 i指向右边界)
若查找 左边界 left ,则执行 j = m - 1;(跳出时 j 指向左边界)
返回值: 应用两次二分,分别查找 right 和 left,最终返回 right - left - 1即可。
效率优化:
以下优化基于:查找完右边界 right = i后,则 nums[j] 指向最右边的 target(若存在)。
查找完右边界后,可用 nums[j] = j 判断数组中是否包含 target,若不包含则直接提前返回 0 ,无需后续查找左边界。
查找完右边界后,左边界 left 一定在闭区间 [0, j] 中,因此直接从此区间开始二分查找即可。

时间复杂度 O(log N): 二分法为对数级别复杂度。
空间复杂度 O(1): 几个变量使用常数大小的额外空间。

class Solution {
    public int search(int[] nums, int target) {
        // 搜索右边界 right
        int i = 0, j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] <= target) i = m + 1;
            else j = m - 1;
        }
        int right = i;
        // 若数组中无 target ,则提前返回
        if(j >= 0 && nums[j] != target) return 0;
        // 搜索左边界 right
        i = 0; j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] < target) i = m + 1;
            else j = m - 1;
        }
        int left = j;
        return right - left - 1;
    }
}

以上代码显得比较臃肿(两轮二分查找代码冗余)。为简化代码,可将二分查找右边界 rightright 的代码 封装至函数 helper() 。

本质上看, helper() 函数旨在查找数字 tartar 在数组 numsnums 中的 插入点 ,且若数组中存在值相同的元素,则插入到这些元素的右边。

class Solution:
    def search(self, nums: [int], target: int) -> int:
        def helper(tar):
            i, j = 0, len(nums) - 1
            while i <= j:
                m = (i + j) // 2
                if nums[m] <= tar: i = m + 1
                else: j = m - 1
            return i
        return helper(target) - helper(target - 1)

53-2、0~n-1中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

示例 1:输入: [0,1,3]
输出: 2
示例 2:输入: [0,1,2,3,4,5,6,7,9]
输出: 8

方法一:遍历数组只要比较数组下标和该下标对应的值即可,再排除缺失0和缺失最后一项两个特殊情况。
时间复杂度: O(N)

class Solution {
    public int missingNumber(int[] nums) {
        if (nums[0]==1) return 0;
        for (int i = 0;i<nums.length;i++){
            if (nums[i]!=i) return i;
        }
        return nums.length;
    }
}

方法二:(二分法)比较中间值与下标的相等关系,相等将更改左边界,不相等更改右边界。
时间复杂度 O(log N): 二分法为对数级别复杂度。
空间复杂度 O(1): 几个变量使用常数大小的额外空间。

class Solution {
    public int missingNumber(int[] nums) {
        int i = 0, j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] == m) i = m + 1;
            else j = m - 1;
        }
        return i;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值