面试题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
- 哈希表计数
- 排序后中位数
- 摩尔投票法(本题最优解法)
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. 数组中的逆序对
题目
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
方法一:归并排序思想
关键是在合并时的操作。合并时关于逆序对有三种情况:
- 左区间中逆序对个数
- 右区间中逆序对个数
- 跨区间的逆序对个数
假设我们有两个已排序的序列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;
}
}