Unit 6 数组和矩阵
Q1:螺旋遍历矩阵
Q2:将矩阵顺时针旋转90度
Q3:之字形打印矩阵
Q4:求数组中无序连续子数组的长度
Q5:数组中出现次数超过一半的数字
Q6:搜索二维矩阵
Q7:求最长可整合子数组的长度
Q8:未排序数组中累加和为给定值k的最长子数组长度
Q9:未排序正数数组中累加和为k的最长子数组长度
Q10:自然数数组的排序
Q11:按奇偶排序数组
Q12:连续子数组的最大和
Q13:子矩阵的最大累加和
Q14:寻找任意峰值数组
Q15:子数组的最大累乘积
Q16:边界都是1的最大正方形大小
Q17:不包含本位置值的累乘数组
Q18:缺失的最小正整数
Q19:寻找数组中重复的数字
Q20:寻找旋转升序数组中的最小值
Q21:矩阵中的单词
Q22:机器人在矩阵中的运动范围
Q23:使数组中奇数排在偶数前面
Q24:把数组排成最小的数
Q25:数组中的逆序对
Q26:统计数字在排序数组中出现的次数
Q27:数组中只出现一次的数字
Q28:买卖股票的最佳时机
Q29:找出数组中第k大的值
unit 6 Q1:螺旋遍历矩阵(顺时针)
Leetcode 54 难度:中等 剑指offer 第29题 牛客链接
时间复杂度O(N),额外时间复杂度O(N)
date:2019/10/08
思路:
1.按圈层打印矩阵,一圈接一圈。
用左上角坐标(tR,tC)和右下角坐标(bR,bC)来确定一个矩阵;
2.每圈置curR、curC用于遍历,初值为tR和tC
分为三种情况:
- (1).只有一行(tR== bR),从左至右遍历一次
- (2).只有一列(tC== bC),从上至下遍历一次
- (3).正常情况,开始绕圈:
先curC一直向右走,走到bC
再curR一直向下走,走到bR
再curC一直向左走,走到tC
再curR一直向上走,走到tR
3.每走完一圈后更新矩阵,tR++,tC++,bR- -,bC- -
代码:
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> list = new ArrayList<>();
if(matrix==null||matrix.length==0||matrix[0].length==0)
return list;
int tR=0,tC=0,bR=matrix.length-1,bC=matrix[0].length-1;
int curR=0,curC=0;
for(; tR<bR && tC<bC ;tR++,tC++,bR--,bC--){
//从[tR][tC]到[tR][bC]
for(curC=tC;curC<bC;curC++)
list.add(matrix[tR][curC]);
//从[tR][bC]到[bR][bC]
for(curR=tR;curR<bR;curR++)
list.add(matrix[curR][bC]);
//从[bR][bC]到[bR][tC]
for(curC=bC;curC>tC;curC--)
list.add(matrix[bR][curC]);
//从[bR][tC]到[tR][tC]
for(curR=bR;curR>tR;curR--)
list.add(matrix[curR][tC]);
}
//只有一行
if(tR==bR)
for(curC=tC;curC<bC+1;curC++)
list.add(matrix[tR][curC]);
//只有一列
else if(tC==bC)
for(curR=tR;curR<bR+1;curR++)
list.add(matrix[curR][tC]);
return list;
}
unit 6 Q2:将矩阵顺时针旋转90度
Leetcode 48 难度:中等
要求:额外时间复杂度为O(1)
date:2019/10/09
思路:
还是按圈层遍历的思路,一层一层的转。对一层的每个位置都转一遍。如图所示
代码:U6Q2_rotateMatrix.java
unit 6 Q3:之字形打印矩阵
要求额外时间复杂度为O(1)
date:2019/10/10
思路:
1.还是分圈层遍历。不用之处在于,(tR,tC)指向矩阵的右上,(bR,bC)指向矩阵的左下,初值均为(0,0)
2.每层遍历打印(tR,tC)和(bR,bC)之间即斜对角线上的点即可。使用isPos决定正着打印还是反着打印。
3.矩阵更新规则:
(tR,tC):右上角先沿着矩阵第一行向右移动(tC++),到头后再沿着矩阵最后一列向下移动(tR++);
(bR,bC):左下角先沿着矩阵第一列向下移动(bR++),到头后再沿着矩阵最后一行向右移动(bC++);
右上角(tR,tC)走到矩阵的右下角时停止。
示例:
代码:U6Q3_zigZagMatrix.java
unit 6 Q4:求数组中无序连续子数组的长度
给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
你找到的子数组应是最短的,请输出它的长度。
Leetcode 581 难度:简单
要求时间复杂度为O(N),额外空间复杂度为O(1)
date:2019/10/11
思路:
想一想,如果一个点比它左边的最大值小,那该点肯定不符合升序,是无序子数组里边的;
同理,如果一个点比它右边的最小值大,那该点肯定也不符合升序,也是无序子数组里边的。
实现:
两趟遍历。
- 1.从左往右遍历,找不符合升序(比左边最大值max还要小)的点的下标,该点指向无序子数组的最右边,存为subRight
- 2.从右往左遍历,找不符合升序(比右边最小值min还要大)的点的下标,该点指向无序子数组的最左边,存为subLeft
(subRight-subLeft+1)即为无序子数组的长度,返回之。
代码:U6Q4_unsortedSubArr.java
unit 6 Q5:数组出现次数超过一半的数字
剑指offer 39 Leetcode 169 难度:简单
date:2019/10/12 date:2020/02/14更新!!!
思路:
方法1(可):遍历数组用hashtable存储各元素的出现次数,记录出现最多的数mode和它的出现次数count,每次遍历更新之。
时间复杂度O(N),额外空间复杂度O(N)
方法2(不可):
2020/02/14更新:既然此方法时间复杂度O(nlogn),还有额外空间复杂度。为啥不能先O(nlogn)排好序,再遍历一遍找出现次数大于(n/2)的次数呢?还没有空间复杂度。。。
分治算法递归求解:
int mode_recur(int[] arr,int left,int right){
//返回arr[left…right]内的众数}
int countInRange(int[] arr,int num,int left,int right){
//返回arr[left…right]内num的出现次数}
每一层的mode_recur()都将当前数组arr[left…right]从中间一分为二。
- 1.如果子数组只有一个元素则众数就是该元素,返回之。
- 2.递归调用mode_recur()求得左边数组的众数 leftMode,递归调用mode_recur()求得右边数组的众数rightMode;
- 3.调用countInRange()求得左众数 leftMode 在左子数组arr[left…mid]中的出现次数,
调用countInRange()求得右众数 rightMode 在右子数组arr[mid+1…right]中的出现次数
左右子数组的众数中,出现次数较多者即为当前数组的众数,返回之。
时间复杂度O(nlogn): 会求解量个长度为n/2的子问题,并做两遍长度为n的遍历。T(n)=2T(n/2)+2n,解得T(n)=O(nlogn)
额外空间复杂度O(logn):该递归树是平衡的,从根到叶节点的长度为logn
方法三(推荐):(2020/02/14更新)
如果该数出现次数超过一半,则该数出现次数大于其他数出现次数之和。则此方法遍历数组时设置一个res和次数times。遍历到arr[i]等于res时,times++;反之times- -。times减到0时,重置res并置times为1。如果出现次数超过一半的数存在,则遍历后剩下的res即是该数。
与剑指offer原书不同的是,牛客网OJ上的题设,有可能不存在出现次数超过一半的数。这时候需要再遍历一遍数组并记录res的出现次数,做个验证即可。
该方法时间复杂度O(n),额外空间复杂度O(1)
代码:
public int MoreThanHalfNum_Solution(int [] array) {
if(array==null||array.length==0)
return 0;
int times=1,res=array[0];
for(int i=1;i<array.length;i++){
if(times==0){
res=array[i];
times=1;
}else{
if(array[i]==res)
times++;
else
times--;
}
}
//和剑指offer原书不同之处在于,他有可能不存在出西安次数超过一半的数字。所以做一个验证
return isLegal(array,res)?res:0;
}
private boolean isLegal(int[] arr,int res){
int times=0;
for(int i=0;i<arr.length;i++){
if(arr[i]==res)
times++;
}
return times>(arr.length/2);
}
unit 6 Q6:搜索二维矩阵
剑指offer 4 Leetcode 74 难度:中等
判断M
×
\times
×N的矩阵matrix中是否存在目标值target。
该矩阵的特点:
1.每行升序排列
2.每行的第一个数大于前一行的最后一个数
date:2019/10/13
思路:
M
×
\times
×N的有序矩阵可以理解为长度为M
×
\times
×N的有序数组,改动二分查找,以时间复杂度O(log(M
×
\times
×N))完成
实现:
1.置左边界left为0,右边界right为(M
×
\times
×N-1)
2.while(left<=right):
- (1).置中间序号pivot=(left+right)/2;
- (2).中间序号对应到数组中即arr[pivot]的值 等于 matrix[pivot/n][pivot%n]
- (3).target等于中间值,找到了,返回true;
target比中间值大,则向右找即left=pivot+1;
target比中间小,则向左找即right=pivot-1.
3.出while循环还没找到,返回false。
date:2020/05/10更新:
剑指offer 4 是这样的。差不多的意思,对每行求一个二分查找
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
代码:
public class Solution {
public boolean Find(int target, int [][] array) {
if(array==null||array.length==0||array[0].length==0)
return false;
//对每一行做一次二分查找
for(int i=0;i<array.length;i++){
//二分查找
int low=0;
int high=array.length-1;
while(high>=0&&low<=high){
int mid=(low+high)/2;
if(array[i][mid]==target){
return true;
}else if(array[i][mid]>target){
high=mid-1;
}else{
low=mid+1;
}
}
}
return false;
}
}
unit 6 Q7:求最长可整合子数组的长度
先给出可整合数组的定义: 如果一个数组arr在排序之后,从最小值到最大值的顺序中,每相邻两个数之间差的绝对值都为1,则arr为可整合数组。 例如: arr = {5,3,4,6,2},再排序之后为:{2,3,4,5,6},排序后符合每相邻两个数之间差的绝对值都为1,所以arr是可整合数组。 给定一个整形数组arr,请返回其中长度最大的可整合子数组的长度。
[5,0,1,2,4,3,9],最长可整合子数组为[5,0,1,2,4,3],所以返回6
[6,7,3,0,1,2,4,7],最长可整合子数组为[3,0,1,2,4],所以返回5
要求:如果数组长度为N,时间复杂度请达到O(
N
2
N^2
N2)
date:2019/10/14
思路:
核心是可整合数组的优化判断法:如果子数组无重复元素且(max-min+1)=子数组的长度,则该子数组为可整合数组
实现:
方法一:暴力解法,穷举所有子数组O(
N
2
N^2
N2),判断是否是可整合数组O(N
l
o
g
2
log_2
log2N),总时间复杂度为O(
N
3
N^3
N3
l
o
g
2
log_2
log2N),略。
方法二:判断是否是可整合数组时采用优化判断法O(1),总时间复杂度为O(
N
2
N^2
N2)
1.两层for循环遍历arr,
第一层用i固定住左边,设置最大值max和最小值min,设置hashSet型set判断重复。
第二层用j向右遍历,每次j移动都更新max和min。
如果当前arr[j]在set中出现过,直接break,否则将arr[j]添加入set。
如果当前(max-min+1)==(j-i+1),则当前arr[i…j]是可整合数组,长度为(j-i+1),更新最长的maxLen
2.两层for循环之后,maxLen即为最长可整合子数组的长度,更新之。
代码:U6Q7_longestIntrgSubArr.java
unit 6 Q8:未排序数组中累加和为给定值k的最长子数组长度
Leetcode 325 难度:中等 (付费题)
参考博客:数组中累加和为定值K的最长子数组长度
给定一无序数组arr,求arr的所有子数组中累加和为k的最长子数组长度
时间复杂度:O(N),额外空间复杂度O(N)
date:2019/10/15
思路:
设s(i)为arr[0…i]的累加和,s[j]即为arr[0…j]的累加和
则子数组arr[j+1,i]的累加和=s[i]-s[j]。
理论上,算出s[0]、s[1]…s[n-1],下标大的减下标小的,即可求出所有的子数组长度。
如果采用暴力方法,两层for循环遍历求出符合(s[i]-s[j])==k的子数组记录maxLen,时间复杂度O(N^2)
现在采用哈希表记录存之前的每个(k,v)=(s[j],j),只用时间复杂度O(N)
实现:
1.遍历前设置hashTable存(key,value)=(arr[0…j]的累加和,j)
向hashtable中添加(0,-1) (因为子数组若以下标0开头,hashtable中找不到key=0的项,出现错误)
置maxLen记录最长子数组的长度,初值为0
置sum记录当前数组arr[i]的长度,初值为0
2.一轮for循环遍历,
- (1).更新sum=sum+arr[i],即更新s[i]
- (2).想找arr[j+1…i]累加和为k,
则置need_sj计算当前需要前数组arr[0…j]的累加和等于多少,need_sj=sum-k。 - (3).如果need_sj在hashtable中存在,取其下标j=hashtable.get(need_sj)(此时说明arr[j+1…i]是累加和为k的数组)
更新maxLen,maxLen取maxLen和(i-j)中较大者 - (4).如果hashtable中没有sum即s[i],将当前(sum,i)加入hashtable
3.出for循环后记得最长子数组长度,返回maxLen
代码:U6Q8_longestEqualToKSubArr.java
unit 6 Q9:未排序正数数组中累加和为k的最长子数组长度
在 U6Q8的问题上,增加数组是正数这一条件。
时间复杂度O(N),额外空间复杂度O(1)
date:2019/10/16
参考博客:数组中累加和为定值K的最长子数组长度
思路:
滑动窗口机制,arr[left…right]表示子数组。
先固定左边不动,右边扩展。当前数组累加和小于k,则一直向右扩展,直到子数组累加和大于k为止。
这时左边向左收缩一个,累加和相应变小,再右边扩张,如此反复至right到头为止,每一次累加和等于k时更新maxLen。
实现:
1.置left,right分别指向子数组的左右,初值为0。置sum记录子数组累加和,初值为0。置maxLen记录子数组最大长度,初值为0。
2.right不到头时while循环:
- (1).sum<k:将arr[right]加入sum,右边向外扩,right++
- (2).sum=k:更新maxLen;arr[left]移出sum,左边向里缩,left++
- (3).sum>k:arr[left]移出sum,左边向里缩,left++
3.返回maxLen。
代码:U6Q9_longestEqualToSubArr2.java
unit 6 Q10:自然数数组的排序
给定一个长度为N的整型数组arr,其中有N个互不相等的自然数1-N,请实现arr的排序,但是不要把下标0-(N-1)位置上的数通过直接赋值的方式替换成1-N
要求:时间复杂度为O(N),额外空间复杂度为O(1)
date:2019/10/19
思路:
for循环连着遍历:
如果arr[i]==i+1:下一位
如果arr[i]!=i+1:
(1想去位置0的位置)arr[i]想去arr[i]-1的位置,
将arr[arr[i]-1]和arr[i]对换即可,
while循环直到换到arr[i]==i+1时,再i++遍历下一个
奇奇怪怪一道题,看起来简单而已
代码:U6Q10_naturalNumSort.java
unit 6 Q11:按奇偶排序数组
Leetcode 922 难度:简单
date:2019/10/20
给定一个非负整数数组 A, A 中一半整数是奇数,一半整数是偶数。
对数组进行排序,以便当 A[i] 为奇数时,i 也是奇数;当 A[i] 为偶数时, i 也是偶数。
你可以返回任何满足上述条件的数组作为答案。
思路:
找到一个偶数位是奇数的前提下,找奇数位上的偶数,找到之后再互换。
实现:
- 1.for循环里套while循环,for循环遍历偶数位,while遍历奇数位找奇数位上的偶数
- 2.置i指向偶数位置初值为0,置j指向奇数位置初值为1.
- 3.for循环用i遍历偶数位,当i碰到奇数时,停下进入while循环:
while循环j遍历奇数位,当j碰到偶数时,停。交换arr[i]和arr[j]。 - 4.继续for循环直到i走到头为止。
代码:U6Q11_sortArrByParity.java
unit 6 Q12:连续子数组的最大和
剑指offer 42 牛客链接 Leetcode 53 难度:简单
要求时间复杂度O(N),空间复杂度O(1)
思路:
动态规划(?)
max存储最大值,sum存储累加和。
- 1.for遍历arr,sum为正(sum对结果有增益效果)一直对sum累加。
- 2.sum变成负数就不继续加了(因为此时sum对结果无增益效果),重新计数将arr[i]赋给arr[i]
- 3.更新max
- 4.出for循环返回max
代码:
public int FindGreatestSumOfSubArray(int[] arr) {
int sum=0,max=Integer.MIN_VALUE;
for(int i=0;i<arr.length;i++){
if(sum<0)
sum=arr[i];//sum<0,当前累加和不能对结果产生增益(结果子数组肯定不包含当前sum)
else
sum=sum+arr[i];//当前sum是有用的
max=Math.max(sum,max);//最大的sum存在max中
}
return max;
}
unit 6 Q13:子矩阵的最大累加和
leetcode 363 难度:困难
date:2019/10/22
思路:
把待求子矩阵压缩到一行,用Q12最大子序和的方法求该行最大累加和,即为待求子矩阵的最大累加和。
两重循环遍历子矩阵O(
N
2
N^2
N2),Q12的方法O(N),总时间复杂度O(
N
2
N^2
N2)
代码:U6Q13_maxSubMatrix.java
unit 6 Q14:寻找任意峰值数组
Leetcode 162 难度:中等
峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其下标。数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
date:2019/10/25
思路:
方法一:遍历一遍找结果,找到第一个符合条件的返回,时间复杂度O(N),空间复杂度O(1)
方法二:二分查找,时间复杂度O(logN),空间复杂度O(1)
1.先判断左右两边即arr[0],arr[len-1]是不是峰值
2.置left初值为1,right初值为(len-2),开始while循环二分查找
3.置mid=(left+right)/2,当mid比左右两边都大时,返回mid
当arr[mid-1]<arr[mid],mid正处于上升坡度,峰值在右,向右爬,left=mid-1;
否则,mid正处于下降坡度,峰值在左,向左爬,right=mid+1。
代码:U6Q14_findPeakIndex.java
nbsp;
unit 6 Q15:子数组的最大累乘积
date:2019/10/27
Leetcode 152 难度:中等
思路:
遍历数组时计算当前最大值。
1.置imax表示以当前节点结束的子数组的最大累乘积,初值为arr[0];
置imin表示以当前节点结束的子数组的最小累乘积,初值为arr[0];
置max记录最大累乘积,初值为MIN_VALUE
2.
遍历数组arr,更新imax,imin,max。更新规则:
如果当前arr[i]为正,则imax从(imaxarr[i])和arr[i]中取,imin从(iminarr[i])和arr[i]中取
如果当前arr[i]为负,arr[i]乘的越小积越大,反过来,imax从(iminarr[i])和arr[i]中取,imin从(imaxarr[i])和arr[i]中取
每回更新最大值max
代码:U6Q15_maxProduct.java
unit 6 Q16:边界都是1的最大正方形大小
Leetcode 1139 难度:中等
给你一个由若干 0 和 1 组成的二维矩阵M,请你找出边界全部由 1 组成的最大 正方形 子网格,并返回该子网格中的元素数量。如果不存在,则返回 0。
date:2019/10/28
思路:
常规思路 O(N^4):
1.对矩阵M[m][n]中的每一位置点M[i][j]。O(N^2)
2.寻找以该点为左上角的正方形,这种正方形有 min(m-i,n-j) 个。O(N)
3.检查每个正方形的四条边是否全为1。O(4N)=O(N)
空间换时间的思路O(N^ 3),空间复杂度O(N^2)
改变常规思路中的第三步。
1.设置right[][]矩阵,right[i][j]表示M[i][j]右边有几个连续的1;
设置down矩阵,down[i][j]表示M[i][j]下边有几个连续的1。
2.
Q:如何判断以len为边长的正方形四条边是否全为1?
A:right[i][j]>=len:上边合格
down[i][j]>=len:左边合格
right[i+len-1][j]>=len:下边合格
down[i][j+len-1]>=len:右边合格
3.
Q:如何构造right矩阵、down矩阵?
A:(1).从右下角开始向上,设置right、down矩阵的最右边。
设置规则:
如果M[i][n-1]==1:则right[i][n-1]=1,down[i][n-1]=down[i+1][n-1]+1;
否则M[i][n-1]==0:则right[i][n-1]=0,down[i][n-1]=0;
(2).从右下角开始向左,设置right、down矩阵的最下边。
设置规则:
如果M[m-1][j]==1:则 right[m-1][j]=right[m-1][j+1]+1,
down[m-1][j]=1;
否则M[m-1][j]==0:则 right[m-1][j]=0;
down[m-1][j]=0;
(3).从右下角开始遍历,填满剩下的right、down矩阵。
填充规则:
如果M[i][j]==1:
right[i][j]=right[i][j+1]+1,down[i][j]=down[i+1][j]+1;
如果M[i][j]==0:
right[i][j]=0,down[i][j]=0。
真的麻烦,不知道搞这么复杂干嘛。
代码:U6Q16_largest1BorderedSquare.java
unit 6 Q17:不包含本位置值的累乘数组
给定一个数组arr,返回不包含本位置值的累乘数组
例如,arr=[2,3,1,4],返回[12, 8, 24, 6],即除自己外,其他位置上的累乘
要求:时间复杂度为O(n),额外空间复杂度为O(1)
难度:简单
date:2019/10/30
思路:
1.遍历数组,算出全部非0元素的累乘积all;计算0元素的个数count0
2.
如果没有0元素,总累乘积除以当前值即可,res[i]=all/arr[i]
如果0元素只有一个,则0元素的点的res[i]为all,其余点均为0;
如果0元素大于1,则res所有位置都为0;
date:2020/02/25 更新:
题目再加限定:不让用除法
剑指offer 66 牛客链接
构建辅助数组C[i]=A[0]*…*A[i-1]*A[i],O(n)
构建辅助数组D[i]=A[i]A[i+1]…*A[len-1],O(n)
这样通过B[i]=A[i-1]*C[i+1],O(n),求得B
//不能使用除法的方法:
import java.util.ArrayList;
public class Solution {
//构建辅助数组C[i]=A[0]*...*A[i-1]*A[i],O(n)
//构建辅助数组D[i]=A[i]*A[i+1]*...*A[len-1],O(n)
//这样B[i]=A[i-1]*C[i+1],O(n)
public int[] multiply(int[] A) {
int len=A.length;
int[] B=new int[len];
int[] C=new int[len];
int[] D=new int[len];
//建立C[i]
//temp暂存累乘的积
for(int i=0,temp=1;i<len;++i){
temp=temp*A[i];
C[i]=temp;
}
//如法炮制建立D[i]
for(int i=len-1,temp=1;i>=0;--i){
temp=temp*A[i];
D[i]=temp;
}
//按照公式B[i]=C[i-1]*C[i+1]构建B[i]
for(int i=0;i<len;++i){
int left=(i==0)?1:C[i-1];
int right=(i==len-1)?1:D[i+1];//边界情况
B[i]=left*right;
}
return B;
}
}
unit 6 Q18:缺失的最小正整数
给定一个未排序的整数数组,找出其中没有出现的最小的正整数。
例:[1,2,0],返回3
[-2,-3,-1,-4],返回1
[7,8,9,11,12],返回1
Leetcode 41 难度:困难
date:2019/10/31
思路:
先"排序",“排序”后第一个不符合要求的点arr[i],(i+1)即为缺失的最小正整数。
排序规则:arr[i]一定要存值为(i+1)的元素
1.“排序”:
从i=0开始for循环遍历arr,将arr[i]的值num换到arr[num-1]上去,一直换换换,换到arr[i]的值为(i+1)或不在范围内(怎么也排不到)停止
2.找不符合的arr[i]
再遍历一遍arr,找到不符合arr[i]!=i+1的,返回i+1。
3.如果能跳出第二遍遍历,说明arr每个元素都符合arr[i]==i+1,返回(len+1)。
代码:U6Q18_firstMissingPos.java
unit 6 Q19:寻找数组中重复的数字
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
例:输入: [1,3,4,2,2] 输出: 2
输入: [3,1,3,4,2] 输出: 3
剑指offer 3 Leetcode 287 难度:中等
date:2019/11/5
方法一:
我让1必须出现在0的位置上,2必须出现在1的位置上。得到的规律:arr[i]位置上的值为i+1;i须出现在arr[i-1]位置上.
1.每轮for循环使得arr[i]位置上的值必须为i+1。
2.for循环里的while循环把当前值cur=arr[i]放在arr[cur-1]的位置上,只有满足当前arr[i]==i+1时才会中止while循环。
3.while循环中遇到arr[cur-1]==cur,则cur就是重复的那个,返回cur。
4.出for循环,肯定哪出错了,返回-1。
代码:
class Solution {
public int findDuplicate(int[] arr) {
//1必须出现在0的位置上。得到的规律:
//arr[i]位置上的值为i+1;i须出现在arr[i-1]位置上
int len=arr.length;
//每轮遍历使得arr[i]位置上的值必须为i+1
for(int i=0;i<len;++i){
//此位置满足
if(arr[i]==i+1)
continue;
//不满足则进while循环找,找到arr[i]==i+1为止
while(arr[i]!=i+1){
int cur=arr[i];//cur应放在arr[cur-1]位置上
//cur想放的地儿重复了,找到你了
if(arr[cur-1]==cur)
return cur;
//arr[i]与arr[cur-1]互换
swap(arr,i,cur-1);
//一定能换到arr[i]==i+1
}
}
return -1;
}
void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
方法二(不移动数组):
二分查找法,找重复的数出现在哪一边。时间复杂度O(Nlog2N)。
class Solution {
//如果正常的left到right无重复,则<=mid的个数等于mid的大小
//例:1~6无重复,mid=3,<=mid的个数应该等于mid。
//如果<=mid的个数大于mid,则重复的数出现在1~mid之间;
//如果<=mid的个数等于mid,则重复的数在mid右边,mid+1~right之间。
public int findDuplicate(int[] arr) {
int len=arr.length;
int left=1,right=len;//刚开始,重复数字在left~right之间
while(left<right){
int mid=(left+right)/2;//中间的数
int curSmaller=0;//<=mid的个数
//统计arr中<=mid的个数
for(int i=0;i<len;++i){
if(arr[i]<=mid)
curSmaller++;
}
//小于等于mid的数多了,left~mid不正常,要去左边left~mid中间找
if(curSmaller>mid)
right=mid;
else
//小于等于mid的数相等,left~mid正常,去mid+1~right中间找
left=mid+1;
}
return left;
}
}
unit 6 Q20:寻找旋转升序数组中的最小值
剑指offer 11 Leetcode 153(数组中元素不可重复) 难度:中等
Leetcode 154(数组中元素可重复) 难度:困难
要求:
假设按照升序排序的数组在预先未知的某个点上进行了旋转,请找出其中最小的元素。
例:数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2]。寻找后者的最小元素为0。
date:2019/12/5
思路:
最简单的方法:全遍历一遍,时间复杂度O(n);改写二分查找,可以做到时间复杂度O(log2n)
将旋转升序数组分为两个部分,最小值其实就是第二个数组的首元素,我们要找到第二个数组首元素下标i
1.while循环,二分查找,不断缩小left和right范围
2.返回条件:找分界点,分界点右边的即为所求。有可能mid落在分界点左或分界点右,需要分类讨论
- A[mid]>A[mid+1]:mid+1是i,返回A[mid+1]
- A[mid-1]>A[mid]:mid是i,返回A[mid]
3.缩小left和right的范围
- A[mid]>A[right]:mid比最右边大,则mid一定在第一个数组,i满足mid<i<=right。向右找,执行left=mid+1
- A[mid]<A[right]:mid比最右边小,则mid一定在第二个数组,i满足left<=i<=mid。向左找,执行right=mid
- A[mid]==A[right]:mid等于最右边,将mid向左挪一个。执行right=right-1
代码:
public int findMin(int[] A){
int left=0,right=A.length-1;
while(left<=right) {
int mid = (left + right) / 2;
//返回条件:返回分界点右,第二数组之首
if (A[mid] > A[mid + 1])
return A[mid + 1];
if (A[mid - 1] > A[mid])
return A[mid];
if (A[mid] > A[right])
//mid比最右边大,则mid一定在第一个数组。
//则最小值下标i一定满足mid<i<=right,向右找i,执行left=mid+1
left = mid + 1;
else if (A[mid] < A[right])
//mid比最右边小,则mid一定在第二个数组。
//i满足left<=i<=mid,向左找i,right=mid
right = mid;
else
//A[mid]==A[right] 一样大,right向左挪一格。
right = right - 1;
}
return -1;
}
unit 6 Q21:矩阵中的单词
剑指offer 12 Leetcode 79 难度:中等
要求:
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
例:
board =[
[‘A’,‘B’,‘C’,‘E’],
[‘S’,‘F’,‘C’,‘S’],
[‘A’,‘D’,‘E’,‘E’]],
给定 word = “ABCCED”, 返回 true.
给定 word = “SEE”, 返回 true.
给定 word = “ABCB”, 返回 false.
date:2019/12/6
思路:
改写深度优先搜索DFS,每次遍历的时候,如果遍历到当前M[i][j]与words[start]对上了,则向上、下、左、右四个方向试探,看这四个方向的能不能与words[start+1]对上。
dfs会一直向下走,只有words中的所有字母都对上了,才会一路返回上来,返回true;中间有一个没对上,只能一路返回false。
要注意,进入匹配上下左右的递归dfs时,对当前visited[i][j]置为true,如果M[i][j]上下左右都没匹配成功,说明这个点虽然遍历过但这次没用上,置其visited[i][j]为false
这道题不简单,复习看代码,看注释。
代码:
public boolean exist(char[][] matrix,String word){
char[] words=word.toCharArray();
int m=matrix.length,n=matrix[0].length;//m:长 n:宽
boolean[][] visited=new boolean[m][n];
//每个点开头都试一次
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(dfs(matrix,i,j,words,0,visited))
return true;
}
}
//出了for循环,dfs还没true过。没找到,返回false
return false;
}
//遍历到matrix[i][j],看matrix[i][j]是不是words[start],如果是,接着试旁边的点
public boolean dfs(char[][] matrix,int i,int j,char[] words,int start,boolean[][] visited){
//i,j坐标不合法 or start不合法,false
if(i>=matrix.length || j>=matrix[0].length || i<0 || j<0 || start>=words.length)
return false;
//如果该点遍历过了,不用再遍历,false
if(visited[i][j]) return false;
char cur=words[start];
//如果当前对上了
if(cur==matrix[i][j]){
//words找到最后一个了,终止dfs递归
if(start==words.length-1)
return true;
//还没找完呢,则向上下左右找
visited[i][j]=true;
//上
if(dfs(matrix,i-1,j,words,start+1,visited)) return true;
//上走不通则下
if(dfs(matrix,i+1,j,words,start+1,visited)) return true;
//左
if(dfs(matrix,i,j-1,words,start+1,visited)) return true;
//右
if(dfs(matrix,i,j+1,words,start+1,visited)) return true;
//走到这还没返回,说明M[i][j]点的上、下、左、右都不行
visited[i][j]=false;
}
//当前没对上,或者之前的上下左右都没走通。返回false
return false;
}
unit 6 Q22:机器人在矩阵中的运动范围
剑指offer 13
有一m行n列的矩阵。一个机器人从坐标(0,0)的格子开始移动,每次都可以上、下、左、右移动一格,但如果行坐标和列坐标的数位之和大于k,该格子不能被进入。求问能进入多少个格子。
例:k=18时:(35,37)可进入,因为3+5+3+7=18;(35,38)不可进入,因为3+5+3+8=19>18
date:2019/12/7
思路:
跟Q21题很像,用深度优先搜索,从(0,0)开始,如果当前(i,j)可以进入,则尝试其上下左右四个方向能否进入,将其能入的计数,最后相加。还需要将整数逐位相加,这一步是基本功难度不大。
代码:
public int robotCount(int m,int n,int k){
int[][] matrix=new int[m][n];
boolean[][] visited=new boolean[m][n];
return dfs(matrix,0,0,k,visited);
}
private int dfs(int[][] matrix,int i,int j,int k,boolean[][] visited){
if(i>=matrix.length || j>=matrix[0].length || i<0 || j<0)
return 0;
//访问过了
if(visited[i][j])
return 0;
int count=0;
//如果该点合法
if(isValid(i,j,k)){
count++;
visited[i][j]=true;
//看看上下左右,能加就加进去
int up=dfs(matrix,i-1,j,k,visited);
int down=dfs(matrix,i+1,j,k,visited);
int left=dfs(matrix,i,j-1,k,visited);
int right=dfs(matrix,i,j+1,k,visited);
count=count+up+down+left+right;
}
return count;
}
private boolean isValid(int i,int j,int k){
int sum=0;
//对i位置累加
while(i/10!=0){
sum+=i%10;
i=i/10;
}
sum+=i;
//对j位置累加
while(j/10!=0){
sum+=j%10;
j=j/10;
}
sum+=j;
return (sum<=k);
}
#### unit 6 Q23:使数组中奇数排在偶数前面
date:2020/02/08
- 普通版:不要求保持奇数和奇数,偶数和偶数之间的相对位置不变。
剑指offer 20 Leetcode 905 难度:简单
模仿快速排序思想。左碰偶停,右碰奇停,i小于j,两边互换。
public void reOrderArray(int [] array) {
int i=0,j=array.length-1;
while(i<j){
while(i<j&&!isEven(i))
i++;//从前面找到一个偶数
while(i<j&&isEven(j))
j--;//从后面找到一个偶数
if(i<j){
//交换
int temp=array[i];
array[i]=array[j];
array[j]=temp;
//i,j各进一步
i++;
j--;
}
}
}
//是否是偶数
boolean isEven(int num){
if(num%2==0)
return true;
else
return false;
}
- 加强版:要求保持奇数和奇数,偶数和偶数之间的相对位置不变。
牛客链接
模仿插入排序的思想。每轮遍历,找到从i开始的第一个偶数。从该偶数开始,数组的后半部分全部前移;再该偶数插在最后。
遍历结束条件:i>=len-k,k是已插过的偶数数量。i遍历是用来找偶数的,i都找到已插好的偶数中去了,则排序结束。
public void reOrderArray(int [] array) {
int i=0,k=0;//k:已插入k个偶数到最后了
while(i<array.length-k){
while(i<array.length-k&&!isEven(array[i]))
i++;//找到第一个偶数
if(i<array.length-k){
int temp=array[i];
//将该偶数之后的所有,都向前挪一个
for(int j=i;j+1<array.length;j++)
array[j]=array[j+1];
array[array.length-1]=temp;//把该偶数插到最后
k++;//又插好一个偶数
}
}
}
//是否是偶数
boolean isEven(int num) {
if (num % 2 == 0)
return true;
else
return false;
}
#### unit 6 Q24:把数组排成最小的数
剑指offer 45 牛客链接
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为"321323"。
date:2020/02/16
思想:
把int[]数组转换成字符串数组。然后对字符串数组排序,排序后合成一个字符串输出。
Q:排序时如何比较两个不等长字符串的大小?
A:把两字符串a、b合起来比较
ab>ba:a>b
ba>ab:b>a
ab=ba:a=b
代码:
public String PrintMinNumber(int [] numbers) {
String res="";
if(numbers==null||numbers.length==0)
return res;
int len=numbers.length;
String[] str=new String[len];
for(int i=0;i<len;i++)
str[i]=String.valueOf(numbers[i]);//转成str类型的
bubbleSort(str);
for(int i=0;i<len;i++)
res=res+str[i];
return res;
}
//ab>ba:a>b
//ba>ab:b>a
//ab=ba:a=b
private int compareAtoB(String A,String B){
String ab=A+B,ba=B+A;
return ab.compareTo(ba);//A<B:<0 A=B:=0 A>B:>0
}
private void bubbleSort(String[] str){
int len=str.length;
for(int i=0;i<len-1;i++){
boolean flag=false;//本趟是否发生交换
for(int j=len-1;j>i;j--){
if(compareAtoB(str[j-1],str[j])>0){
String temp=str[j-1];
str[j-1]=str[j];
str[j]=temp;
flag=true;
}
}
if(!flag)
break;
}
}
#### unit 6 Q25:数组中的逆序对
剑指offer 51 Leetcode链接
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例:
输入: [7,5,6,4]
输出: 5
date:2020/02/19
思路:
链接:
归并排序的改进,把数据分成前后两个数组(递归分到每个数组仅有一个数据项),
合并数组,合并时,出现前面的数组值arr[i]大于后面数组值arr[j]时,则arr[i]大于arr[mid+1]到arr[j]的所有值。这些数有(j-mid)个。如果arr[i]<arr[j],j向后退。每次选出一个最大的放到copy数组里。
参考剑指Offer,看看代码吧,注释挺全。
//Leetcode 通过版
int[] copy;
public int reversePairs(int[] arr) {
if(arr==null||arr.length==0)
return 0;
//对copy数组进行初始化
copy=new int[arr.length];
for(int i=0;i<arr.length;i++)
copy[i]=arr[i];
int count=reverseCore(arr,0,arr.length-1);
return count;
}
//返回arr[low]到arr[high]区间内逆序对个数
int reverseCore(int[] arr,int low,int high){
if(low==high)
return 0;
int mid=(low+high)/2;
//先向下递归
int leftCount=reverseCore(arr,low,mid);
int rightCount=reverseCore(arr,mid+1,high);
//递归体
int count=0;
int i=mid,j=high;//i遍历左子数组,j遍历右子数组;都从最右边开始
int locCopy=high;//复制数组的下标
while(i>=low&&j>=mid+1){
//左边比右边大。
//由于左、右子数组都是有序的,则arr[i]比arr[mid+1]..arr[j]都大,可以和他们都组成逆序对。
//arr[mid+1]到arr[j]共有j-(mid+1)+1=(j-mid)个数
if(arr[i]>arr[j]){
count+=j-mid;
copy[locCopy]=arr[i];//较大者存进copy的右边。其实就是排序结果
locCopy--;i--;
}else{
//左边比右边小,当前arr[i]和arr[j]都不能组成逆序对了
//只更新排序数组
copy[locCopy]=arr[j];
locCopy--;j--;
}
}
//出了while循环,一定是左、右两子数组有一个遍历到头了
//把左、右子数组剩下的部分(已排序)放入copy即可
//以下两个while只会执行一个
while(i>=low){
copy[locCopy]=arr[i];
locCopy--;i--;
}
while(j>=mid+1){
copy[locCopy]=arr[j];
locCopy--;j--;
}
//将copy[low]..copy[high]赋给arr[low]..arr[high]使其有序
for(int k=low;k<=high;k++)
arr[k]=copy[k];
//可以返回arr[low]...arr[high]的逆序对总数了
return leftCount+rightCount+count;
}
unit 6 Q26:统计数字在排序数组中出现的次数
剑指offer 53_1 牛客链接
date:2020/02/19
思想:
暴力方法,时间复杂度O(n)。
由于是排序数组,想到用归并排序。
修改归并排序退出的规则,先找到该数第一次出现的下标firstK,再找到该数最后一次出现的下标lastK。(lastK-firstK+1)该数的出现次数。
由于归并排序的时间复杂度尾O(log2N),改进的方法时间复杂度尾O(log2N)
代码:
public int GetNumberOfK(int [] array , int k) {
int firstK=firstKIndex(array,k);
int lastK=lastKIndex(array,k);
return (firstK==-1||lastK==-1)?0:lastK-firstK+1;
}
int firstKIndex(int[] arr,int k){
int low=0,high=arr.length-1;
int mid;
while(low<=high){
mid=(low+high)/2;
if(arr[mid]==k&&(mid-1<0||arr[mid-1]!=k))
return mid;
if(arr[mid]<k)
low=mid+1;//往右走
else
high=mid-1;//arr[mid]相等或者大于k,都往左走
}
return -1;
}
int lastKIndex(int[] arr,int k){
int low=0,high=arr.length-1;
int mid;
while(low<=high){
mid=(low+high)/2;
if(arr[mid]==k&&(mid+1>=arr.length||arr[mid+1]!=k))
return mid;
if(arr[mid]>k)
high=mid-1;//向左走
else
low=mid+1;
}
return -1;
}
unit 6 Q27:数组中只出现一次的数字
date:2020/02/20
除了某元素只出现一次以外,其余每个元素均出现两次
Leetcode 136 难度:简单
异或的性质:
- 交换律: a ^ b ^ c == a ^ c ^ b
- 任何数与0异或为自己 0 ^ n == n
- 相同的数异或为0 n ^ n == 0
根据以上异或的三性质,用0开始分别与数组中每个数字异或,最后得到的结果是只出现一次的那个数。(出现两次的数通过交换律放在一起,异或为0)
代码:
class Solution {
public int singleNumber(int[] nums) {
int temp=0;
for(int i=0;i<nums.length;i++)
temp=temp^nums[i];
return temp;
}
}
变形1:除了两个元素只出现一次以外,其余每个元素均出现两次
剑指offer 56_1 牛客链接
不能直接异或得结果,我们可以想到,将数组分为两半,每一半都有一个只出现一次的数字,对两半数组分别做上一题的异或操作即可。
Q:如何实现将数组一分为二,并保证两个只出现一次的数字一边一个?
A:
设只出现一次的数字分别为 a 和 b。
先做一遍异或操作求出a和b的异或值bitResult。
由于该值不为0,则该值的二进制中必有一位是1。再求出该值的二进制从右向左数的第一个为1的下标index。
异或操作是逐位异或的,所以可以断定a和b在从右边数第index位上是不一样的,正因为这样异或结果的index位才是1。而一样的数字的index位一定相同。
所以可以通过index位是否为1,将数组一分为二。一样的数字因为index相同被划分在一起,而a和b因为index位不同而被分开。目的达到。
代码:
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
public void FindNumsAppearOnce(int [] arr,int num1[] , int num2[]) {
int len=arr.length;
if(len==2){
num1[0]=arr[0];
num2[0]=arr[1];
}
//先遍历一遍,获得两个只出现一次的数字的异或值
int temp=0;
for(int i=0;i<len;i++)
temp=temp^arr[i];
int bitResult=temp;//结果存在bitResult里
//找到两出现一次数字的异或值结果的二进制,从右边数第一个1的位置
int index=findIndex(bitResult);
//再遍历一遍
//temp1遍历数组中从右边数第index位是1的数
//temp2遍历数组中从右边数第index位是0的数
int temp1=0,temp2=0;
for(int i=0;i<len;i++)
if(isBit1(arr[i],index))
temp1=temp1^arr[i];
else
temp2=temp2^arr[i];
//temp1,temp2交回数组
num1[0]=temp1;
num2[0]=temp2;
}
//找到异或结果的二进制从右边开始第几个数字是1
private int findIndex(int bitResult){
int index=0;//从右边数第几个
//>>:右移运算符 &:位的”与“运算,对两数逐位做"与"运算
//bitResult最后一位是1时跳出循环
while((bitResult&1)==0){
bitResult=bitResult>>1;//二进制的结果右移一位
index++;
}
return index;
}
//数target的二进制从右边数第index位是否是1
private boolean isBit1(int target,int index){
return ((target>>index)&1)==1;
}
}
unit 6 Q28:买卖股票的最佳时机
剑指offer 63 Leetcode 121 难度:简单
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。注意你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
date:2020/02/24
思路:
这道题本质上是找出一前一后两个数,使(后数-前数)的差值最大
暴力方法能想到,对于每个元素找到一个使差值最大的后数,比较所有差值,返回最大的差值。这种方法时间复杂度O(N^2)
进阶方法想到用动态规划的想法。遍历时设置min记录当前的最小值,maxDif记录最大的差值。这种方法O(N)
代码:
class Solution {
public int maxProfit(int[] prices) {
if(prices==null||prices.length==0)
return 0;
int min=prices[0];//迄今为止的最小值
int maxDif=0;//迄今为止的最大差值
for(int i=1;i<prices.length;++i){
maxDif=Math.max(maxDif,prices[i]-min);//更新最大值
min=Math.min(min,prices[i]);//更新最小值
}
return maxDif;
}
}
unit 6 Q29:找出数组中第k大的值
Leetcode 215 难度:中等
date:2020/05/15
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
快速排序的思想。
第1个最大的元素是arr[len-1],则第K个最大的元素是arr[len-k]
快速排序每趟遍历固定一个,找到(len-k)位置上的值即可
class Solution {
int k;
int res;
boolean isFind=false;//是否找到第k个最大的元素
public int findKthLargest(int[] arr, int k) {
//快速排序的思想,找到第len-k位置,即为第k个最大的元素
this.k=k;
if(arr.length==1)
return arr[0];
quickSort(arr,0,arr.length-1);
return res;
}
void quickSort(int[] arr,int low,int high){
//原来的基础上,加上一个isFind
//快排中是low<high。因为这个需要多一步判断的操作,需要稍微修改
if(low<=high&&!isFind){
int pivot=partition(arr,low,high);
//k=1,取arr[len-1];k=2,取arr[len-2]
if(pivot==arr.length-k){
res=arr[pivot];
isFind=true;
return;
}
quickSort(arr,low,pivot-1);
quickSort(arr,pivot+1,high);
}
}
int partition(int[] arr,int low,int high){
int temp=arr[low];
while(low<high){
while(low<high&&arr[high]>=temp)
high--;
arr[low]=arr[high];
while(low<high&&arr[low]<=temp)
low++;
arr[high]=arr[low];
}
arr[low]=temp;
return low;
}
}