剑指offer——数组+矩阵合集(Leetcode)

面试题03. 数组中重复的数字

题目
这道题很简单,用set和map都可以。

class Solution {
    public int findRepeatNumber(int[] nums) {
        Set<Integer> set=new HashSet<Integer>();
        int repeat=-1;
        for(int num:nums){
            if(!set.add(num)){
                repeat=num;
                break;
            }
        }
        return repeat;
    }
}

这里是一个很好的想法:
如果没有重复数字,那么正常排序后,数字i应该在下标为i的位置,所以思路是重头扫描数组,遇到下标为i的数字如果不是i的话(假设为m),那么我们就拿与下标m的数字交换。在交换过程中,如果有重复的数字发生,那么终止返回ture。

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 * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

暴力解法就是直接遍历查找。
这里可以利用数组的递增关系,从第一行最右边的值开始查找,如果当前元素等于目标值,则返回 true。如果当前元素大于目标值,则移到左边一列。如果当前元素小于目标值,则移到下边一行。

class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        int n=matrix.length;
        if(n==0) return false;
        int m=matrix[0].length;
        int i=0,j=m-1;
        while(i<n&&j>=0){
            if(matrix[i][j]==target){
                return true;
            }else if(matrix[i][j]<target){
                i++;
            }else if(matrix[i][j]>target){
                j--;
            }
        }
        return false;
    }
}

面试题29. 顺时针打印矩阵

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

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

方法一:模拟
关键是用dir={{0, 1}, {1, 0}, {0, -1}, {-1, 0}}四个方向一步一步走完顺时针就可以。

复杂度分析:

  • 时间复杂度:O(mn),矩阵中的每个元素都要被访问一次。
  • 空间复杂度:O(mn),需要创建一个大小为 m ×n 的矩阵visited记录每个位置是否被访问过。
class Solution {
    public int[] spiralOrder(int[][] matrix) {
        if(matrix==null || matrix.length==0 ||matrix[0].length==0){
            return new int[0];
        }
        int n=matrix.length,m=matrix[0].length;
        int total=n*m;
        int[] ans=new int[total];
        boolean[][] visit=new boolean[n][m];
        int[][] dir={{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

        int i=0,j=0;
        int dirIndex=0;
        for(int k=0;k<total;k++){
            ans[k]=matrix[i][j];
            visit[i][j]=true;
            int nex_i=i+dir[dirIndex][0];
            int nex_j=j+dir[dirIndex][1];
            if(nex_i<0 || nex_i>=n || nex_j<0 || nex_j>=m || visit[nex_i][nex_j]==true){
                dirIndex=(dirIndex+1)%4;
            }
            i=i+dir[dirIndex][0];
            j=j+dir[dirIndex][1];
        }

        return ans;
    }
}

方法二:按层模拟
复杂度分析:

  • 时间复杂度:O(mn),矩阵中的每个元素都要被访问一次。
  • 空间复杂度:O(1)
    在这里插入图片描述
class Solution {
    public int[] spiralOrder(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return new int[0];
        }
        int rows = matrix.length, columns = matrix[0].length;
        int[] order = new int[rows * columns];
        int index = 0;
        int left = 0, right = columns - 1, top = 0, bottom = rows - 1;
        while (left <= right && top <= bottom) {
            for (int column = left; column <= right; column++) {
                order[index++] = matrix[top][column];
            }
            for (int row = top + 1; row <= bottom; row++) {
                order[index++] = matrix[row][right];
            }
            if (left < right && top < bottom) {
                for (int column = right - 1; column > left; column--) {
                    order[index++] = matrix[bottom][column];
                }
                for (int row = bottom; row > top; row--) {
                    order[index++] = matrix[row][left];
                }
            }
            left++;
            right--;
            top++;
            bottom--;
        }
        return order;
    }
}

面试题11. 旋转数组的最小数字

题目
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。

输入:[3,4,5,1,2]
输出:1

使用二分的方式来找出最小的数字。应该算是对二分的一个变体,因为这里并不是排序的数组。

复杂度分析:
时间复杂度 O(log_2 N);在特例情况下(例如[1,1,1,1]),会退化到 O(N)。
空间复杂度 O(1)。

class Solution {
    public int findMin(int[] numbers) {
        int l=0,r=numbers.length-1;
        while(l<r){
            int m=(l+r)/2;
            if(numbers[m]<numbers[r]){
                r=m;
            }else if(numbers[m]>numbers[r]){
                l=m+1;
            }else{
                r--;
            }
        }
        return numbers[l];
    }
}

面试题12. 矩阵中的路径

题目
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径。

[[“a”,“b”,“c”,“e”],
[“s”,“f”,“c”,“s”],
[“a”,“d”,“e”,“e”]]

但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。

DFS。注意每次查找某条路径后要把visit数组置false。

class Solution {
    int[] dx={1,-1,0,0};
    int[] dy={0,0,1,-1};
    boolean[][] visited;
    int n,m;

    public boolean exist(char[][] board, String word) {
        n=board.length;
        m=board[0].length;
        visited=new boolean[n][m];

        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(board[i][j]==word.charAt(0)&&!visited[i][j]){
                    if(dfs(board,i,j,1,word)) return true;
                    visited[i][j]=false;
                }
            }
        }
        return false;
    }

    private boolean dfs(char[][] board,int x,int y,int len,String word){
        visited[x][y]=true;
        if(len==word.length()){
            return true;
        }
        for(int i=0;i<4;i++){
            int xx=x+dx[i],yy=y+dy[i];
            if(xx>=0&&xx<n&&yy>=0&&yy<m&&!visited[xx][yy]){
                if(board[xx][yy]==word.charAt(len)){
                    if(dfs(board,xx,yy,len+1,word)){
                        return true;
                    }
                    visited[xx][yy]=false;
                }
            }
        }
        return false;
    }
}

面试题21. 调整数组顺序使奇数位于偶数前面

题目
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。

输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。

参考题解
给出两种方法:

  • 首尾双指针
    头指针left指向奇数,尾指针right指向偶数,否则交换
  • 快慢双指针
    快指针fast 向前搜索奇数位置,慢指针low 指向下一个奇数应当存放的位置
//首尾双指针
class Solution {
    public int[] exchange(int[] nums) {
        int l=0,r=nums.length-1;
        while(l<r){
            while((l<r)&&(nums[l]&1)!=0){
                l++;
            }
            while((l<r)&&(nums[r]&1)!=1){
                r--;
            }
            int tmp=nums[l];nums[l]=nums[r];nums[r]=tmp;
            l++;r--;
        }
        return nums;
    }
}
//快慢双指针
class Solution {
    public int[] exchange(int[] nums) {
        int fast=0,low=0;
        while(fast<nums.length){
            if ((nums[fast] & 1)!=0) {
                int tmp=nums[low];nums[low]=nums[fast];nums[fast]=tmp;
                low ++;
            }
            fast ++;
        }
        return nums;
    }
}

面试题39. 数组中出现次数超过一半的数字

题目
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。

输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2

非常妙的题解

  1. 哈希表计数
  2. 排序后中位数
  3. 摩尔投票法(本题最优解法)
class Solution {
    public int majorityElement(int[] nums) {
        int cnt=0;
        int ans=0;
        for(int num:nums){
            if(cnt==0){
                ans=num;
            }
            cnt+=(num==ans?1:-1);
        }
        return ans;
    }
}

面试题42. 连续子数组的最大和

题目
输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

方法一:动态规划

f(i):以第i个数字结尾的最大子区间和。
ans=max(f(i))
f(i)=max(f(i-1)+nums[i],nums[i])
由于每个状态只与前一个状态有关,可以使用滚动数组进行优化,时间效率可以提升很多。

// class Solution {
//     public int maxSubArray(int[] nums) {
//         int dp[]=new int[nums.length];
//         dp[0]=nums[0];
//         int maxans=nums[0];
//         for(int i=1;i<nums.length;i++){
//             dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
//             maxans=Math.max(maxans,dp[i]);
//         }
//         return maxans;
//     }
// }

//滚动数组优化
class Solution {
    public int maxSubArray(int[] nums) {
        int pre=0;
        int maxans=nums[0];
        for(int i=0;i<nums.length;i++){
            pre=Math.max(pre+nums[i],nums[i]);
            maxans=Math.max(pre,maxans);
        }
        return maxans;
    }
}

方法二:分治

使用线段树的思想。
官方题解对这点解释的很好。

class Status{
    int lsum; // [l,r]内以 l 为左端点的最大子段和
    int rsum; // [l,r]内以 r 为右端点的最大子段和
    int isum; // [l,r]内区间和
    int msum; // [l,r]内最大子区间和
    public Status(int l,int r,int i,int m){
        lsum=l;rsum=r;isum=i;msum=m;
    }
}

class Solution {
    Status Pushup(Status lsub,Status rsub){
        int lsum=Math.max(lsub.lsum,lsub.isum+rsub.lsum);
        int rsum=Math.max(rsub.rsum,rsub.isum+lsub.rsum);
        int isum=lsub.isum+rsub.isum;
        int msum=Math.max(Math.max(lsub.msum,rsub.msum),lsub.rsum+rsub.lsum);
        return new Status(lsum,rsum,isum,msum);
    }

    Status getMax(int l,int r,int[] nums){
        if(l==r) return new Status(nums[l],nums[l],nums[l],nums[l]);
        int m=(l+r)/2;
        Status lsub= getMax(l,m,nums);
        Status rsub= getMax(m+1,r,nums);
        return Pushup(lsub,rsub);
    }

    public int maxSubArray(int[] nums) {
        return getMax(0,nums.length-1,nums).msum;
    }
}

面试题45. 把数组排成最小的数

题目
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

输入: [10,2]
输出: “102”

输入: [3,30,34,5,9]
输出: “3033459”

实质:自定义排序规则的排序题
排序判断规则: 设 nums任意两数字的字符串格式 x 和 y ,则

  • 若拼接字符串 x + y > y + x,则 m > n;
  • 反之,若 x + y < y + x,则 n < m

1. 自己实现排序
这里使用快排的排序方法。回顾快排思想

class Solution {
    public String minNumber(int[] nums) {
        String[] strs = new String[nums.length];
        for(int i = 0; i < nums.length; i++)
            strs[i] = String.valueOf(nums[i]);
        fastSort(strs, 0, strs.length - 1);
        StringBuilder res = new StringBuilder();
        for(String s : strs)
            res.append(s);
        return res.toString();
    }
    void fastSort(String[] strs, int l, int r) {
        if(l >= r) return;
        int i = l, j = r;
        String tmp = strs[i];
        while(i < j) {
            while((strs[j] + tmp).compareTo(tmp + strs[j]) >= 0 && i < j) j--;
            strs[i]=strs[j];
            while((strs[i] + tmp).compareTo(tmp + strs[i]) <= 0 && i < j) i++;
            strs[j]=strs[i];
        }
        strs[i] = tmp;
        fastSort(strs, l, i - 1);
        fastSort(strs, i + 1, r);
    }
}

2. 使用内置方法排序

class Solution {
    public String minNumber(int[] nums) {
        String[] strs = new String[nums.length];
        for(int i = 0; i < nums.length; i++) 
            strs[i] = String.valueOf(nums[i]);
        Arrays.sort(strs, (x, y) -> (x + y).compareTo(y + x));
        StringBuilder res = new StringBuilder();
        for(String s : strs)
            res.append(s);
        return res.toString();
    }
}

面试题51. 数组中的逆序对

题目
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

官方题解

方法一:归并排序思想

关键是在合并时的操作。合并时关于逆序对有三种情况:

  1. 左区间中逆序对个数
  2. 右区间中逆序对个数
  3. 跨区间的逆序对个数
    假设我们有两个已排序的序列L,R等待合并,用指针 lPtr = 0 指向 L 的首部,rPtr = 0 指向 R 的头部。
    合并的过程中计算逆序对的数量的时候,只在 lPtr 右移的时候计算,在此之前, lPtr 指向的数字一直比 rPtr 大,因此 rPtr指向的一串序列都与当前的lPtr组合成逆序对,当lPtr 指向的数字比 rPtr 小时,它比 R中 [0 … rPtr - 1] 的其他数字大,[0 … rPtr - 1] 的其他数字本应当排在 lPtr 对应数字的左边,但是它排在了右边,所以这里就贡献了 rPtr 个逆序对。
class Solution {
    public int reversePairs(int[] nums) {
        int n=nums.length;
        if(n==0) return 0;
        int[] tmp=new int[n];
        return mergeSort(nums,tmp,0,n-1);
    }

    private int mergeSort(int[] nums,int[] tmp,int l,int r){
        if(l==r) return 0;

        int mid=(l+r)/2;
        int lsub=mergeSort(nums,tmp,l,mid);
        int rsub=mergeSort(nums,tmp,mid+1,r);
        int inv_count=lsub+rsub;

        if(nums[mid]<=nums[mid+1]){
            return inv_count;
        }
        int i=l,j=mid+1;
        int pos=l;
        while(i<=mid&&j<=r){
            if(nums[i]<=nums[j]){
                tmp[pos++]=nums[i];
                i++;
                inv_count+=(j-(mid+1));
            }
            else{
                tmp[pos++]=nums[j];
                j++;
            }
        }
        for(int k=i;k<=mid;k++){
            tmp[pos++]=nums[k];
            inv_count+=(j-(mid+1));
        }
        for(int k=j;k<=r;k++){
            tmp[pos++]=nums[k];
        }
        for(int k=l;k<=r;k++){
            nums[k]=tmp[k];
        }
        return inv_count;
    }
}

方法二:离散化树状数组
典型的树状数组求逆序对。

class BIT {
private:
    vector<int> tree;
    int n;

public:
    BIT(int _n): n(_n), tree(_n + 1) {}

    static int lowbit(int x) {
        return x & (-x);
    }

    int query(int x) {
        int ret = 0;
        while (x) {
            ret += tree[x];
            x -= lowbit(x);
        }
        return ret;
    }

    void update(int x) {
        while (x <= n) {
            ++tree[x];
            x += lowbit(x);
        }
    }
};

class Solution {
public:
    int reversePairs(vector<int>& nums) {
        int n = nums.size();
        vector<int> tmp = nums;
        // 离散化
        sort(tmp.begin(), tmp.end());
        for (int& num: nums) {
            num = lower_bound(tmp.begin(), tmp.end(), num) - tmp.begin() + 1;
        }
        // 树状数组统计逆序对
        BIT bit(n);
        int ans = 0;
        for (int i = n - 1; i >= 0; --i) {
            ans += bit.query(nums[i] - 1);
            bit.update(nums[i]);
        }
        return ans;
    }
};

面试题53 - I. 在排序数组中查找数字 I

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

这里注意是排序数组,所以不要简单地使用map遍历统计每个数字个数,采用二分思想,找到该值出现的范围段(left,right),出现次数为right-left-1。
时间复杂度:O(logN)

这里注意,两次二分分别查找left和right的区间时,两个边界范围的取值是不一样的。在查找right边界时,最后结果会是l>r,因此right应该取l,此时r对应的值是在target中的。left同理。

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

        l=0;r=nums.length-1;
        while(l<=r){
            int mid=(l+r)/2;
            if(nums[mid]<target){
                l=mid+1;
            }
            else r=mid-1;
        }
        int left=r;

        return right-left-1;
    }
}

这种写法比较冗余,用了两段差不多的二分,也可以判断target和target-1的右边界,相减依然是结果。参考题解

class Solution {
    public int search(int[] nums, int target) {
        return helper(nums, target) - helper(nums, target - 1);
    }
    int helper(int[] nums, int tar) {
        int i = 0, j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] <= tar) i = m + 1;
            else j = m - 1;
        }
        return i;
    }
}

面试题53 - II. 0~n-1中缺失的数字

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

输入: [0,1,3]
输出: 2

看到排序数组的查找问题,一定要首先想到二分。和上面一题一样。

class Solution {
    public int missingNumber(int[] nums) {
        int l=0,r=nums.length-1;
        while(l<=r){
            int mid=(l+r)/2;
            if(nums[mid]==mid){
                l=mid+1;
            }
            else{
                r=mid-1;
            }
        }
        return l;
    }
}

面试题56 - I. 数组中数字出现的次数

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

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

利用异或性质:相同的两个数字异或结果为0。
官方题解已经写的很清楚了

两个关键的思考点:

  • 如果除了一个数字以外,其他数字都出现了两次,那么如何找到出现一次的数字:全员进行异或操作即可
  • 这一方法如何扩展到找出两个出现一次的数字:把所有数字分成两组,使得
    (1)两个只出现一次的数字在不同的组中;
    (2)相同的数字会被分到相同的组中。
    这里如何分组的思想也很巧妙,将所有数字异或后,得到的值实际为两个只出现了一次的数字a和b的异或结果,找到结果中第一位为1的位,证明在这一位这两个值是不同的,因此,对于其他数字,按照这一位是1还是0进行划分,即可将数字分为上面需要的两组。
class Solution {
    public int[] singleNumbers(int[] nums) {
        // 求出两个不同数字a^b的值
        int sum=0;
        for(int num:nums){
            sum^=num;
        }
        // 得到a^b中第一个1所在位置
        int k=1;
        while((k&sum)==0){
            k<<=1;
        }
        // 按照1所在位置分类
        int a=0,b=0;
        int res[]=new int[2];
        res[0]=0;res[1]=0;
        for(int num:nums){
            if((num&k)==0){
                res[0]^=num;
            }else{
                res[1]^=num;
            }
        }
        return res;
    }
}

面试题56 - II. 数组中数字出现的次数 II

题目
在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

输入:nums = [3,4,3,3]
输出:4

关键做法:位运算。
这道题是根据每一位的1的个数做判断,如果是出现三次,那么在某一位上,1的个数为3,即所有的1的个数是3的倍数,否则证明这一位还出现了只出现一次的那个数。

class Solution {
    public int singleNumber(int[] nums) {
        int res=0;
        for(int i=0;i<32;i++){
            int k=(1<<i);
            int count=0;
            for(int num:nums){
                if((num&k)!=0){
                    count++;
                }
            }
            if(count%3==1){
                res|=k;
            }
        }
        return res;
    }
}

面试题66. 构建乘积数组

题目
给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B 中的元素 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

对于A[i],用两次遍历分别算出它的左右乘积即可。

class Solution {
    public int[] constructArr(int[] a) {
        int n=a.length;
        if(n==0){
            return a;
        }
        int sum=1;
        int[] b=new int[n];
        int[] ans=new int[n];
        
        ans[n-1]=1;
        for(int i=n-2;i>=0;i--){
            ans[i]=ans[i+1]*a[i+1];
        }

        for(int i=0;i<n;i++){
            b[i]=ans[i]*sum;
            sum=sum*a[i];
        }
        
        return b;
    }
}

面试题13. 机器人的运动范围

题目

class Solution {
    int cnt=0;
    int[] dx={1,-1,0,0};
    int[] dy={0,0,1,-1};
    boolean[][] visit;
    int m,n;

    public int movingCount(int mm, int nn, int k) {
        m=mm;n=nn;
        visit=new boolean[m][n];
        dfs(0,0,k);
        return cnt;
    }

    void dfs(int x,int y,int k){
        visit[x][y]=true;
        cnt+=1;
        if(x==m-1 && y==n-1){
            return;
        }
        for(int i=0;i<4;i++){
            int xx=x+dx[i];
            int yy=y+dy[i];
            if(xx>=0&&xx<m&&yy>=0&&yy<n&&visit[xx][yy]==false){
                if(get(xx)+get(yy)<=k){
                    dfs(xx,yy,k);
                }
            }
        }
    }

    int get(int x){
        int sum=0;
        while(x>0){
            sum+=x%10;
            x/=10;
        }
        return sum;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值