OptimalSolution(5)--数组和矩阵问题(1)简单
一、转圈打印矩阵
题目:给定一个整型矩阵matrix,按照转圈的方式打印它。
要求:额外空间复杂度为O(1)
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
打印结果为:1 2 3 4 8 12 16 15 14 13 9 5 6 7 11 10
思路:矩阵分圈处理问题。用矩阵中左上角的坐标(tR,rC)和右下角的坐标(dR,dC)就可以表示一个子矩阵。
例如:(0,0)和(3,3)表示的是原来矩阵的最外层,此时打印:1 2 3 4 8 12 16 15 14 13 9 5
然后令tR和tC加1,dR和dC减1,即(1,1)和(2,2)表示原矩阵的内层,此时打印:6 7 11 10
然后令tR和tC加1,得到(2,2),而dR和dC减1,得到(1,1),此时左上角坐标跑到了右下角坐标的右方或下方,整个过程停止
public void printEdge(int[][] m, int tR, int tC, int dR, int dC) { if (tR == dR) { // (tR,tC)和(dR,dC)在同一行上 for(int i = tC; i <= dC; i++) { System.out.print(m[tR][i] + " "); } } else if (tC == dC) { // (tR,tC)和(dR,dC)在同一列上 for(int i = tR; i <= dR; i++) { System.out.print(m[i][tC] + " "); } } else { // (tR,tC)在左上,(dR,dC)在右下 int curC = tC; int curR = tR; while (curC != dC) { // 先打印最上面一行 System.out.print(m[tR][curC] + " "); curC++; } while (curR != dR) { // 先打印最右面一行 System.out.print(m[curR][dC] + " "); curR++; } while (curC != tC) { // 先打印最下面一行 System.out.print(m[dR][curC] + " "); curC--; } while (curR != tR) { // 先打印最左面一行 System.out.print(m[curR][tC] + " "); curR--; } } } public void spiralOrderPrint(int[][] matrix) { int tR = 0; int tC = 0; int dR = matrix.length - 1; int dC = matrix[0].length - 1; while (tR <= dR && tC <= dC) { printEdge(matrix, tR++, tC++, dR--, dC--); } } public static void main(String[] args) { int[][] matrix= {{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,16}}; new e1().spiralOrderPrint(matrix); }
二、将正方形矩阵顺时针转动90°
题目:给定一个N×N的矩阵matrix,把这个矩阵调整成顺时针90°后的形式
1 2 3 4 13 9 5 1 5 6 7 8 14 10 6 2 9 10 11 12 15 11 7 3 13 14 15 16 → 16 12 8 4
要求:额外空间复杂度为O(1)
思路:仍然采用分圈的调整方式。
例如:(0,0)和(3,3)时,让1,4,16,13为一组,让1占据4的位置,4占据16的位置,16占据13的位置,13占据1的位置。然后2,8,15,9和3,12,14,5同理
(1,1)和(2,2)时,直接调整6 7 10 11即可
如果子矩阵的大小为M×M,那么一共就有M-1组需要调整
package Chapter8; public class e2 { public void rorateEdge(int[][] m, int tR, int tC, int dR, int dC) { int times = dR - tR; // 需要调整的组数 int tmp = 0; for (int i = 0; i != times; i++) { tmp = m[tR][tC + i]; m[tR][tC + i] = m[dR - i][tC]; m[dR - i][tC] = m[dR][dC - i]; m[dR][dC - i] = m[tR + i][dC]; m[tR + i][dC] = tmp; } } public void rorate(int[][] matrix) { int tR = 0; int tC = 0; int dR = matrix.length - 1; int dC = matrix[0].length - 1; while (tR < dR) { rorateEdge(matrix, tR++, tC++, dR--, dC--); } } public static void main(String[] args) { int[][] matrix= {{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,16}}; new e2().rorate(matrix); for (int i = 0; i < matrix.length; i++) { for (int j = 0; j < matrix[0].length; j++) { System.out.print(matrix[i][j] + " "); } System.out.println(); } } }
三、“之”字形打印矩阵
题目:给定一个矩阵matrix,按照“之”字形的方式打印矩阵
1 2 3 4 5 6 7 8 9 10 11 12 打印:1 2 5 9 6 3 4 7 10 11 8 12
解法:
第一步:上坐标(tR,tC)初始为(0,0),先沿着矩阵第一行移动(tC++),当到达第一行最右边的元素后,再沿着矩阵最后一列移动(tR++)
第二步:下坐标(dR,dC)初始为(0,0),先沿着矩阵第一列移动(dR++),当到达第一列最下边的元素时,再沿着矩阵最后一行移动(dC++)
第三步:上坐标与下坐标同步移动,每次移动后的上坐标与下坐标的连线就是矩阵中的一条斜线,打印斜线上的元素即可
第四步:如果上次斜线是从左下到右上打印的,这次一定是从右上向左下打印的,反之亦然。把打印的方向用boolean表示,每次取反即可。
例如:第一条斜线:1,第二条斜线:2 5,第三条斜线:3 6 9,第四条斜线:4 7 10,第五条斜线:8 11,第六条斜线:12
package Chapter8; public class e3 { public void printLevel(int[][] m, int tR, int tC, int dR, int dC, boolean f) { if (f) { while (tR != dR + 1) { System.out.print(m[tR++][tC--] + " "); } } else { while (dR != tR - 1) { System.out.print(m[dR--][dC++] + " "); } } } public void printMatrixZigZag(int[][] matrix) { int tR = 0, tC = 0, dR = 0, dC = 0; int endR = matrix.length - 1; int endC = matrix[0].length - 1; boolean fromUp = false; while (tR != endR + 1) { printLevel(matrix, tR, tC, dR, dC, fromUp); tR = tC == endC ? tR + 1 : tR; // 没走到最右边之前,往右走 tC = tC == endC ? tC : tC + 1; // 走到了最右边,往下走 dC = dR == endR ? dC + 1 : dC; // 没走到最下边之前,往下走 dR = dR == endR ? dR : dR + 1; // 走到了最下边,往右走 fromUp = !fromUp; // 反转打印方向 } System.out.println(); } }
四、需要排序的最短子数组长度
题目:给定一个无序数组arr,求出需要排序的最短子数组长度
例如:arr=[1,5,3,4,2,6,7],只有[[5,3,4,2]需要排序,因此返回4
解法:O(N) + O(1)
第一步:初始化变量noMinIndex= -1,从右向左遍历,记录右侧出现过的数的最小值,记为min。同时,如果arr[i]>min,说明如果要整体有序,min值必定会挪到arr[i]的左边。用noMinIndex记录最左边出现这种情况的位置。如果遍历完成noMinIndex仍然等于-1,说明不需要排序。
第二步:初始化变量noMaxIndex=-1,从左向右遍历,同理,记录左侧出现过的最大值,同时记录出现最后arr[i]<max的位置。
第三步:返回arr[noMinIndex...noMaxIndex]的长度即可
例如:从右到左,遍历到4>min=2,遍历到3>min=2,遍历到5>min=2,遍历到1,因此最后出现>min的位置就是5所在的位置
public int getMinLength(int[] arr) { if (arr == null || arr.length == 0) { return 0; } int min = arr[arr.length - 1]; int noMinIndex = -1; for (int i = arr.length - 2; i >= 0; i--) { if (arr[i] > min) { noMinIndex = i; } else { min = Math.min(min, arr[i]); } } if (noMinIndex == -1) { return 0; } int max = arr[0]; int noMaxIndex = -1; for (int i = 1; i < arr.length; i++) { if (arr[i] < max) { noMaxIndex = i; } else { max = Math.max(max, arr[i]); } } return noMaxIndex - noMinIndex + 1; }
五、在行列都排好序的矩阵中找数
题目:给定一个行列都还排好序的N×M的整型矩阵matrix和一个整数K,判断K是否在matrix中
0 1 2 5 2 3 4 7 4 4 4 8 5 7 7 9 如果K为7,返回false;如果K为6,返回false
要求:时间复杂度为O(N+M),额外空间复杂度为O(1)
方法1:从矩阵的最右上角开始寻找row=0,col=M-1,然后比较当前matrix[row][col]与K的关系
1.如果与K相等,直接返回true
2.如果比K大,由于列已经排好序,说明下方的数都比K大,令col--,重复2
3.如果比K小,由于行已经排好序,说明左方的数都比K小,令row++,重复2
方法2:从矩阵的最左下角开始寻找row=N-1,col=0,然后比较当前matrix[row][col]与K的关系
1.如果与K相等,直接返回true
2.如果比K大,由于行已经排好序,说明右方的数都比K大,令row--,重复2
3.如果比K小,由于列已经排好序,说明上方的数都比K小,令col++,重复2
public boolean isContains(int[][] matrix, int K) { int row = 0; int col = matrix[0].length - 1; while (row < matrix.length && col > -1) { if (matrix[row][col] == K) { return true; } else if (matrix[row][col] > K) { col--; } else { row++; } } return false; }
六、自然数数组的排序
题目:给定一个长度为N的整型数组arr,其中有N个互不相等的自然数N,实现排序,即arr[index]=index+1
要求:O(N) + O(1)
方法1:从左到右遍历arr,
1.如果arr[i]=i+1,说明不需要调整
2.如果arr[i] != i+1,则需要进行调整。
方式1:跳跃操作。例如:[1,2,5,3,4],arr[2]=5,把5放在arr[4]有[1,2,5,3,5],把4放在arr[3]有[1,2,5,4,5],把3放在arr[2]上有[1,2,3,4,5],即回到了原位置arr[2]
public void sort1(int[] arr) { int tmp = 0; int next = 0; for (int i = 0; i < arr.length; i++) { tmp = arr[i]; while (arr[i] != i + 1) { next = arr[tmp - 1]; arr[tmp - 1] = tmp; tmp = next; } } }
方式2:交换操作。例如:[1,2,5,3,4],arr[2]=5,5和4交换有[1,2,4,3,5],arr[2]=4,4和3交换有[1,2,3,4,5],arr[2]==3,遍历下一个位置
public void sort2(int[] arr) { int tmp = 0; for (int i = 0; i < arr.length; i++) { while (arr[i] != i + 1) { tmp = arr[arr[i] - 1]; arr[arr[i] - 1] = arr[i]; arr[i] = tmp; } } }
七、奇数下标都是奇数或者偶数下标都是偶数
问题:给定一个长度不小于2的数组arr,调整arr使得要么让所有的偶数下标都是偶数,要么让所有的奇数下标都是奇数
要求:O(N) + O(1)
解法:
第一步:设置变量even,表示目前arr最左边的偶数下标,初始时even=0
第二步:设置变量odd,表示目前arr最左边奇数下标,初始时odd=1
第三步:不断检查arr的最后一个数,即arr[N-1],如果arr[N-1]是偶数,交换arr[N-1]和arr[even],然后令even=even+2,如果arr[N-1]是奇数,交换arr[N-1]和arr[odd],然后令odd=odd+2
第四步:如果even或odd大于N,返回。
以[1.8,3,2,4,6]为例,even=0,odd=1, end=6,6和1交换:6 8 3 2 4 1,even=2 end=1,1和8交换:6 1 3 2 4 8,odd=3 end=8,8和3交换:6 1 8 2 4 3,even=4 end=3,3和2交换:6 1 8 3 4 2,odd=5 end=2,2和4交换:6 1 8 3 2 4,even=6 此时even大于等于长度,说明偶数下标已经都是偶数。返回
实质上也就是:如果最后一位是偶数,就将最后一位填到偶数下标,如果最后一位是奇数,就将最后一位填到奇数下标;最后一位相当于身份识别与发送器。
public void swap(int[] arr, int index1, int index2) { int tmp = arr[index1]; arr[index1] = arr[index2]; arr[index2] = tmp; } public void modify(int[] arr) { if (arr == null || arr.length == 0) { return; } int even = 0; int odd = 1; int end = arr.length - 1; while (even <= end && odd <= end) { if ((arr[end] & 1) == 0) { swap(arr, end, even); even += 2; } else { swap(arr, end, odd); odd += 2; } } }
八、子数组的最大累加和问题
题目:给定一个数组arr,返回子数组的最大累加和
例如:arr=[1-2,3,5,-2,6,-1],所有的子数组中,[3,5,-2,6]可以累加出最大的和12,返回12
要求:时间复杂度O(N),空间复杂度O(1)
解法:(1)如果arr中没有正数,那么最大累加和一定是数组中的最大值。(2)如果arr中有正数,从左到右遍历arr,用变量cur记录每一步的累加和,当cur<0时,令cur=0,表示从下一个树开始累加;当cur>=0时,每一次累加和都可能是最大的累加和,用变量max记录cur的最大值即可。
arr=[1-2,3,5,-2,6,-1],cur=0,max=0
cur=1,max=1
cur=-1,说明[1,-2]肯定不是产生最大累加和的子数组的左边部分,cur=0
cur=3,max=3
cur=8,max=8
cur=6,max=8
cur=12,max=12
cur=11,max=1
代码实现:
public int maxSum(int[] arr) { if (arr == null || arr.length == 0) { return 0; } int max = Integer.MIN_VALUE; int cur = 0; for (int i = 0; i < arr.length; i++) { cur += arr[i]; max = Math.max(max, cur); cur = cur < 0 ? 0 : cur; } return max; }
九、不包含本位置值的累乘数组
题目:给定一个整型数组arr,返回不包含本位置值的累成数组
例如:arr=[2,3,1,4],返回[12,8,24,6]。
题目1:时间复杂度为O(N),除需要返回的结果数组外,额外空间复杂度为O(1)
解法:结果数组记为res,所有非零数的乘积记为all,数组中0的数量记为count。如果数组中不含0,则res[i]=all/arr[i]即可;如果数组中有一个0,则res[i]=all,其他位置上都是0;如果数组中0的数量大于1,那么res所有位置上都是0。
public int[] product1(int[] arr) { if (arr == null || arr.length == 0) { return null; } int count = 0; int all = 1; for (int i = 0; i < arr.length; i++) { if (arr[i] != 0) { all *= arr[i]; } else { count++; } } int[] res = new int[arr.length]; if (count == 0) { for (int i = 0; i < res.length; i++) { res[i] = all / arr[i]; } } if (count == 1) { for (int i = 0; i < res.length; i++) { if (arr[i] == 0) { res[i] = all; } } } return res; }
题目2:对时间和空间复杂度的要求不变,且不可以使用除法
解法:
1.生成两个长度和arr一样的数组lr[]和rl[],lr[]表示从左到右的累乘,即lr[i]=arr[0...i]的累乘,rl[]表示从右到左的累乘,即rl[i]=arr[i...N-1]的累乘
2.一个位置上除去自己的累乘,就是自己左边的累乘再乘以自己右边的累乘,即res[i]=lr[i-1]*rl[i+1]。
3.最左边位置上res[0]=rl[1],最右边位置上res[N-1]=lr[N-2]
4.为了避免使用两个额外的数组,可以先把res数组作为辅助计算的数组,然后把res调整成结果数组返回。具体操作是,res表示lr[],tmp表示lr[i]
例如:arr=[2,3,1,4],经过第一步累乘后res=[2,6,6,24],tmp=1 i=3时,res[3]=res[2]*1= 6,tmp=1*arr[3]=4 i=2时,res[2]=res[1]*4=24,tmp = 4*arr[2]=4 i=1时,res[1]=res[0]*4=8,tmp=4*arr[1]=12 i=0时,res[0]=12
代码实现:
public int[] product2(int[] arr) { if (arr == null || arr.length == 0) { return null; } int[] res = new int[arr.length]; int tmp = 1; for (int i = res.length - 1; i > 0; i--) { res[i] = res[i - 1] * tmp; tmp *= arr[i]; } res[0] = tmp; return res; }
十、数组的partition调整
题目1:给定一个有序数组arr,调整arr使得数组的左半部分没有重复元素且升序,例如,arr=[1,2,2,2,3,3,4,5,6,6,7,7,8,8,8,9],调整后arr=[1,2,3,4,5,6,7,8,9...]。要求时间复杂度为O(N),空间复杂度为O(1)
解法:
1.生成变量u,在arr[0...u]上都是无重复且升序的。初始时u=0,这个区域记为A
2.生成变量i,利用i做从左到右的遍历,在arr[u+1...i]上是不保证没有重复元素且升序的区域,初始时i=1,这个区域记为B
3.如果arr[i] != arr[u],说明arr[i]应该加入到A区域里,所有交换arr[u+1]和arr[i],同时u++;如果arr[i]==arr[u],说明arr[i]的值之前已经加入到A区域,此时不用再加入。
以arr=[1,2,2,2,3,3,4,5,6,6,7,7,8,8,8,9]为例,u=0,i=1,A:0...0, B:1...1
i=1,arr[1]=2不等于arr[0]=1,于是交换arr[1]和arr[1],u=1
i=2,arr[2]=2等于arr[1],
i=3,arr[3]=arr[1]
i=4,arr[4]=3不等于arr[u],于是交换3和2,u=2
i=5,arr[5]=3等于arr[u]
i=6,aarr[6]=4不等于arr[u],于是交换4和2...
实质上就是双指针操作,u作为左指针,i作为右指针,如果arr[i]不等于arr[u],就令u右移一位,由于是排好序的,因此像2 2 2 3 3 4这种情况,当交换完成后,即使变成2 3 2 2 3 4时,由于i已经右移了,所以3和u所指的3也会不相等,也就是3后面不会再出现2了。
public void leftUnique(int[] arr) { if (arr == null || arr.length < 2) { return; } int u = 0; int i = 1; while (i != arr.length) { if (arr[i++] != arr[u]) { swap(arr, ++u, i - 1); } } } private void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; }
题目2:给定一个数组arr,其中只可能含有0(红球),1(蓝球),2(黄球)三个值,实现arr的排序。求时间复杂度为O(N),空间复杂度为O(1)
解法:
1.生成变量left,含义是arr[0...left]上都是0,初始时left=-1
2.生成变量index,含义是arr[left+1...index]上都是1,初始时index为0
3.生成变量right,含义是arr[right...N-1]上都是2,初始时right为N
4.index表示遍历到arr的一个位置
(1)如果arr[index]=1,这个值本来就应该在中区,直接index++
(2)如果arr[index]=0,把arr[index]和arr[left+1]交换,同时扩大左区,left++。此时由于arr[left+1]一定等于1,所以直接index++即可。
(3)如果arr[index]=2,把arr[index]和arr[right-1]交换,同时扩大右区,right--。如果原arr[right-1]=1,那么应该到位了,如果原arr[right-1]=0,那么还需要把0 调整到左区,因此综合考虑,index不变继续下一轮的判断比较好。
5.当index==right时,说明中区和右区成功对接,三个区域划分完成。
private void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } public void sort(int[] arr) { if (arr == null || arr.length < 2) { return; } int left = -1; int index = 0; int right = arr.length; while (index < right) { if (arr[index] == 0) { swap(arr, ++left, index++); } else if (arr[index] == 2) { swap(arr, index, --right); } else { index++; } } }
分析执行过程:
L R ↓ ↓ 1 2 0 0 2 2 1 1 2 1 2 0 0 0 ↑ ↓ ↓ 1 0 0 0 2 2 1 1 2 1 2 0 0 2 ↑ ↓ ↓ 0 1 0 0 2 2 1 1 2 1 2 0 0 2 ↑ ↓ ↓ 0 0 1 0 2 2 1 1 2 1 2 0 0 2 ↑
↓ ↓
0 0 0 1 2 2 1 1 2 1 2 0 0 2
↑
↓ ↓
0 0 0 1 0 2 1 1 2 1 2 0 2 2
↑
↓ ↓
0 0 0 0 1 2 1 1 2 1 2 0 2 2
↑
↓ ↓
0 0 0 0 1 0 1 1 2 1 2 2 2 2
↑
↓ ↓
0 0 0 0 0 1 1 1 2 1 2 2 2 2
↑
↓ ↓
0 0 0 0 0 1 1 1 2 1 2 2 2 2
↑
↓ ↓
0 0 0 0 0 1 1 1 2 1 2 2 2 2
↑
↓ ↓
0 0 0 0 0 1 1 1 1 2 2 2 2 2
↑