数组类型总结
135. 分发糖果
注意:本题的核心就是拆解为子问题,然后使用数组的正向遍历和反向遍历特性,进行迭代求解最优解。
class Solution {
public int candy(int[] ratings) {
/**
分析:
每个孩子至少分配1个糖果==》定义一个数组,数组初始化全为1
若i的分数比左右高,i处的孩子比i-1和i+1的糖果数要多。==》分治成左右两个规则,数组的正向反向遍历,最后取最大值进行累加即可
*/
int len = ratings.length;
int[] left = new int[len];
int[] right = new int[len];
Arrays.fill(left,1);
Arrays.fill(right,1);
// 正向遍历数组
for(int i = 1; i < len; i++){
if(ratings[i] > ratings[i - 1]){
left[i] = left[i - 1] + 1;
}
}
int sum = 0;
// 反向遍历数组,这里的j一定要从len-1开始,因为要开始取左右数组的最大值了
for(int j = len - 1; j >= 0; j--){
// 防止下标越界,所以要使用 j!= len - 1
if(j != len -1 && ratings[j] > ratings[j+1]){
right[j] = right[j+1] + 1;
}
sum += Math.max(left[j],right[j]);
}
return sum;
}
}
189. 旋转数组
时间复杂度O(N),空间复杂度O(n),题目要求空间复杂度O(1)
class Solution {
public void rotate(int[] nums, int k) {
/**
分析:
最开始的想法是保存原数组到新数组上,然后遍历新数组的倒数第k个数,输出到原数组上
*/
int len = nums.length;
int[] copy = new int[len];
copy = Arrays.copyOfRange(nums,0,len);
// 对k进行化简
k %= len;
int count = 0;
// 遍历原数组
for(int i = len - k; i < len; i++){
nums[count++] = copy[i];
}
for(int j = 0; j < len - k; j++){
nums[count++] = copy[j];
}
}
}
注意:本题的特例,需要技巧,使用到了翻转数组的特性
class Solution {
public void rotate(int[] nums, int k) {
/**
要求使用原地修改算法,如果是链表类型,那么很容易想到用快慢指针法
本题是数组类型,并且这道题在考研王道中也有出现过,很明显会想到翻转数组(技巧题型)
先整体翻转,再翻转(0,k-1),最后翻转余下的
*/
int len = nums.length;
k %= len;
reverse(nums,0,len - 1);
reverse(nums,0,k-1);
reverse(nums,k,len - 1);
}
public void reverse(int[] nums,int start,int end){
while(start < end){
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
}
228. 汇总区间(双指针的变形)
注意:本题的核心在于使用一个变量i来联系low和high,同时判别low和high的关系,进行拼接字符串。
有点像是快慢指针,这种思想学习学习!!!
第一次没做出来就是使用slow,fast,当fast不满足条件时,无法定位到fast前一个位置。
但是本题就使用一个中间变量i,进行巧妙连接!
class Solution {
public List<String> summaryRanges(int[] nums) {
/**
分析:
若是一个区间范围表,则瞒足后一个数比前一个数大1,否则就是一个独立的数字
基本思路是遍历数组,同时记录首尾指针,是区间范围表,那么将首尾添加到list中,否则直接添加list
当low < high时,是一个区间
当low = high时,是一个数
*/
int len = nums.length;
List<String> res = new ArrayList<>();
int i = 0;
while( i < len){
// 定义low指针随i变化
int low = i;
// 递增i,便于使用num[i] - num[i - 1] == 1语句
i++;
// 找到非连续的点
while( i < len && nums[i] - nums[i - 1] == 1){
i++;
}
// 找到非连续点的前一个
int high = i - 1;
// 初始化字符串为low
StringBuilder sb = new StringBuilder(Integer.toString(nums[low]));
// 判断low与high的关系
if(low < high){
sb.append("->");
sb.append(Integer.toString(nums[high]));
}
// 记得转换为String
res.add(sb.toString());
}
return res;
}
}
605种花问题
注意:第一版是自己写的代码,主要思路是去遍历,然后扣除首尾的边界条件。在进行判断首尾的边界条件时耗费了很大的功夫(面向测试用例编程),这种做法很明显是需要优化的,能不能把首尾的边界条件做一个简单的优化?答案是可以的,给数组前加一个0.数组末加一个0,这种编程思路叫做“防御式编程”
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
/**
分析:题目的意思就是在数组中寻找0,插入n个1。使得每个1之间至少间隔一个
*/
int len = flowerbed.length;
int count = 0;
if(n == 0){
return true;
}else if(n==1 && len == 1 && flowerbed[0] == 0){
return true;
}
else if(n==1 && len == 1 && flowerbed[0] == 1){
return false;
}
if(len < n ){
return false;
}
if(flowerbed[0] == 0 && flowerbed[1] == 0){
flowerbed[0] = 1;
count++;
}
for(int i = 1; i < len - 1; i++){
if(flowerbed[i] == 0 && flowerbed[i - 1] == 0 && flowerbed[i + 1] == 0){
flowerbed[i] = 1;
count++;
}
}
if(flowerbed[len - 2] == 0 && flowerbed[len - 1] == 0){
flowerbed[len - 1] = 1;
count++;
}
if(count >= n){
return true;
}
return false;
}
}
注意:防御式编程思想的运用!唯一的不足就是空间复杂度是O(n)
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
// 防御式编程,首尾两边加个0
int len = flowerbed.length;
int[] temp = new int[len + 2];
// 对temp数组进行赋值操作
for(int i = 1; i <= len; i++){
temp[i] = flowerbed[i-1];
}
// 遍历temp数组,这时候就不需要进行过多的边界判断了
for(int i = 1; i <= len; i++){
if(temp[i]==0&&temp[i-1]==0&&temp[i+1]==0){
//种上花
temp[i] = 1;
n--;
}
}
return n<=0;
}
}
注意:若是还是想用空间复杂度是O(1)的做法,那么就要思考下之前的边界条件能不能再优化(这里的优化值得是算法思想的优化),答案肯定是可以的。对数组遍历,若当前的花坛i已经种植花了,很明显需要到i+2处进行查看是否能种植,若当前花坛是最后一个花坛或者当前花坛和下一个花坛都没有种植,那么当前位置i就可以种植,同时继续到i+2处查看是否能种植(这里解释下为甚最后一个花坛可以种植:若只有单个,最后一个花坛可以种植,若为多个,由于i+2的操作,导致倒数第二个花坛必为0,故可以种植),最后一种情况,若i没有种花,但是i+1种花了,那么肯定是要去i+3的位置去探索了。
总之,比较难想到这种边界条件的判定解法,需要一定的积累。
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
/**
优化边界条件
*/
int len = flowerbed.length;
int i= 0;
while( i < len){
// 当前位置中了花
if(flowerbed[i] == 1){
i += 2;
}else if(i == len - 1 || flowerbed[i] ==0 &&flowerbed[i + 1] == 0) {
// 当前位置可以种,下一个位置也可以种
n--;
i += 2;
}else{
// 当前位置可以种,下一个位置不可以中
i += 3;
}
}
return n <= 0;
}
}
665. 非递减数列
注意:本题技巧性真的强,是要理解测试案例 3 4 2 3然后根据这个测试案例做出相关的赋值操作,依次循环迭代
class Solution {
public boolean checkPossibility(int[] nums) {
/**
看了题解才明白,解决本题的核心就是去理解测试案例 3 4 2 3
以上测试案例是false,那么第一个发现nums[i] < nums[i - 1]时,应该将nums[i] = nums[i - 1],计数+1,然后继续遍历
若计数 > 1,则说明无法改变一个元素
*/
int count = 0;
for(int i = 1; i < nums.length; i++){
if(nums[i] < nums[i - 1]){
// 发现非递增
count++;
//判断count是否超过1
if(count > 1){
return false;
}
// 改变值
if(i - 2 >= 0 && nums[i] < nums[i -2] ){
nums[i] = nums[i - 1];
}
}
}
return true;
}
}
860. 柠檬水找零
class Solution {
public boolean lemonadeChange(int[] bills) {
//统计店员所拥有的5元和10元的数量(20元的不需要统计,
//因为顾客只能使用5元,10元和20元,而20元是没法
// 给顾客找零的)
int five = 0, ten = 0;
for (int bill : bills) {
if (bill == 5) {
//如果顾客使用的是5元,不用找零,5元数量加1
five++;
} else if (bill == 10) {
//如果顾客使用的是10元,需要找他5元,所以
//5元数量减1,10元数量加1
five--;
ten++;
} else if (ten > 0) {
//否则顾客使用的只能是20元,顾客使用20元的时候,
//如果我们有10元的,要尽量先给他10元的,然后再
//给他5元的,所以这里5元和10元数量都要减1
ten--;
five--;
} else {
//如果顾客使用的是20元,而店员没有10元的,
//就只能给他找3个5元的,所以5元的数量要减3
five -= 3;
}
//上面我们找零的时候并没有判断5元的数量,如果5元的
//数量小于0,说明上面某一步找零的时候5元的不够了,
//也就是说没法给顾客找零,直接返回false即可
if (five < 0) {
return false;
}
}
return true;
}
}
941. 有效的山脉数组
注意:以下是我的第一次解法,有很多优化的空间,比如flag重复标记最高点这个操作明显会拉低效率,完全可以在循环结束后进行判断最高点(最高点不能是第一个和最后一个元素)
class Solution {
public boolean validMountainArray(int[] arr) {
/**
分析:
在区间[1,n-2]中找到一个数,使得该数组前半部分升序,后半部分降序
*/
int len = arr.length;
if(len < 3){
return false;
}
int i = 1;
int flag = 0;
while( i < len - 1){
if( arr[i] - arr[i - 1] > 0 ){
// 重复标记flag,flag是最高点
flag = i;
i++;
}else if(arr[i+1] - arr[i] < 0){
i++;
}else{
// 其余情况下,退出循环
break;
}
}
// i到最后一个数,并且最高点符合条件(最高点要大于起点和终点)
if( i == len - 1 && arr[flag] > arr[0] &&arr[flag] > arr[len - 1] ){
return true;
}
return false;
}
}
第二版的代码逻辑性就更好了,最高点在while循环结束后开始判断。
class Solution {
public boolean validMountainArray(int[] arr) {
int len = arr.length;
int i = 0;
while( i < len -1 && arr[i] < arr[i + 1]){
i++;
}
// 判断最高点(不能是第一个元素和最后一个元素)
if(i == 0 || i == len - 1){
return false;
}
while( i < len - 1 && arr[i] > arr[i+1]){
i++;
}
return i==len - 1;
}
}
48. 旋转图像(数组转置+镜像)
注意:使用复制数组,唯一的缺点就是要去推导旋转后下标之间的关系,这种实际上比较耗费时间,从思考上是不难的,但是从推导上是难的,因此更加推荐使用第二种解法,转置+镜像解法
class Solution {
public void rotate(int[][] matrix) {
// 若是没有要求原地旋转,那么完全可以先复制数组,再修改
int len = matrix.length;
int[][] copy = new int[len][len];
for(int i = 0; i < len; i++){
for(int j = 0; j < len; j++){
copy[j][len - i - 1] = matrix[i][j];
}
}
// 修改数组
for(int i = 0; i < len; i++){
for(int j = 0; j < len; j++){
matrix[i][j] = copy[i][j];
}
}
}
}
升级版~!
class Solution {
public void rotate(int[][] matrix) {
/**
分析:
要求原地修改二维数组,通过观察可以发现,第一行数据变成最后一列,第二行数据变成倒数第二列,最后一行数据变成第一列。学习转置代码和镜像代码~~~
总结规律:
情况一:顺时针转 90 度:先转置再左右镜像
1 2 3 7 4 1
4 5 6 8 5 2
7 8 9 9 6 3
情况二:顺时针转 180 度:先上下镜像,再左右镜像(先左右再上下也可)
1 2 3 9 8 7
4 5 6 6 5 4
7 8 9 3 2 1
情况三:顺时针转 270 度:先转置再上下镜像
1 2 3 3 6 9
4 5 6 2 5 8
7 8 9 1 4 7
*/
int len = matrix.length;
// 先转置
for(int i = 0; i < len; i++){
for(int j = 0; j < i; j++){
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// 再左右镜像
int left = 0;
int right = len - 1;
while( left < right){
for(int i = 0; i < len; i++){
int temp = matrix[i][left];
matrix[i][left] = matrix[i][right];
matrix[i][right] = temp;
}
left++;
right--;
}
}
}
54. 螺旋矩阵
注意:矩阵是长方形的时候还需要添加额外的判断条件,防止重复计算~
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
/**
分析:
这道题似曾相识,像是模拟法.分为上下左右四个区块。
没错就是模拟法,和59题思路完全一模一样。
class Solution {
public int[][] generateMatrix(int n) {
//模拟法的应用:如何解决边界条件
//四个边界
int l=0,r=n-1,t=0,b=n-1;
//count存放数值
int count = 1,val = n*n;
//这是结果数组
int [][] res = new int [n][n];
while(count<=val){
//从左往右
for (int i = l; i <= r ; i++) {
res[t][i] = count++;
}
//top边界要下移
t++;
//从上往下
for (int i = t; i <= b ; i++) {
res[i][r] = count++;
}
//right边界要左移
r--;
//从右往左
for (int i = r; i >= l ; i--) {
res[b][i] = count++;
}
//bottom边界要上移
b--;
//从下往上
for (int i = b; i >= t ; i--) {
res[i][l] = count++;
}
//left边界要右移
l++;
}
return res;
}
}
*/
List<Integer> res = new ArrayList<>();
int m = matrix.length;
int n = matrix[0].length;
int left = 0,right = n - 1,top = 0, bottom = m - 1;
int total = m * n;
while(total >= 1){
// total >= 1 是为了应对长方形的,比如一个长方形的长有10,宽只有3,当循环完最后一次从左到右的时候,total已经小于1了,此时的left和right由于循环得很少,向里面缩进得很少,当状态来到从右向左的时候,由于没有total >= 1,又可以执行了,但是之前这些值已经添加了。。
// 上边界
for(int i = left; i <= right&&total >= 1; i++){
res.add(matrix[top][i]);
total--;
}
// 上边界下移
top++;
for(int i = top; i <= bottom&&total >= 1; i++){
res.add(matrix[i][right]);
total--;
}
// 右边界左移
right--;
for(int i = right; i >= left&&total >= 1; i--){
res.add(matrix[bottom][i]);
total--;
}
// 下边界上移
bottom--;
for(int i = bottom; i >= top&&total >= 1; i--){
res.add(matrix[i][left]);
total--;
}
// 左边界右移
left++;
}
return res;
}
}
73. 矩阵置零(标记算法思想)
注意:使用数组,思想比较简单,空间复杂度为o(m+n)
class Solution {
public void setZeroes(int[][] matrix) {
/**
分析:
方案一:使用标记数组法。定义行列数组,若某一元素出现0.则将该行、列数组对应位置标记,然后遍历置0。时间复杂度为o(m*n)空间复杂度为O(m+n)
方案二:使用两个临时变量,标记首行和首列是否存在0,对于非首行首列的元素置于最上方和最左方,然后遍历置0,最后根据首行首列的标记位再次置0
*/
// 标记数组法
int m = matrix.length;
int n = matrix[0].length;
boolean[] rows = new boolean[m];
boolean[] cols = new boolean[n];
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(matrix[i][j] == 0){
// 标记对用的行和列
rows[i] = true;
cols[j] = true;
}
}
}
// 再次遍历
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
// 判断标记数组
if(rows[i] || cols[j] ){
// 更新数组
matrix[i][j] = 0;
}
}
}
}
}
注意:使用标记变量,比较饶一点,空间复杂度o(1)
class Solution {
public void setZeroes(int[][] matrix) {
/**
分析:
方案一:使用标记数组法。定义行列数组,若某一元素出现0.则将该行、列数组对应位置标记,然后遍历置0。时间复杂度为o(m*n)空间复杂度为O(m+n)
方案二:使用两个临时变量,标记首行和首列是否存在0,对于非首行首列的元素置于最上方和最左方,然后遍历置0,最后根据首行首列的标记位再次置0
*/
// 使用两个临时变量标记首行和首列是否有0
boolean flagRow = false;
boolean flagCol = false;
int m = matrix.length;
int n = matrix[0].length;
// 标记临时变量
for(int i = 0; i < m; i++){
if(matrix[i][0] == 0){
// 标记首列
flagCol = true;
break;
}
}
for(int i = 0; i < n; i++){
if(matrix[0][i] == 0){
// 标记首行
flagRow = true;
break;
}
}
// 标记非首行非首列的元素到最左边和最上边
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(matrix[i][j] == 0){
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// 遍历,处理最左边和最上边的0元素
for(int i = 1; i < m; i++){
if(matrix[i][0] == 0){
// 行置0
for(int j = 1; j < n; j++){
matrix[i][j] = 0;
}
}
}
for(int j = 1; j < n; j++){
if(matrix[0][j] == 0){
// 列置0
for(int i = 1; i < m; i++){
matrix[i][j] = 0;
}
}
}
// 处理首行首列的标记位
if(flagRow){
for(int i = 0; i < n; i++){
matrix[0][i] = 0;
}
}
if(flagCol){
for(int i = 0; i < m; i++){
matrix[i][0] = 0;
}
}
}
}
118. 杨辉三角(抽象思维能力)
注意:本题是一个简单,但却十分的体现了抽象思维的能力。遇到一个问题,如何将文字语言转换为数学语言,如何将数学语言转换为数据结构算法。这是核心negligible!
比如杨辉三角中的首列和末列都是1,那么如何将就可以抽象成 j == 0 和 i == j,然后再去计算~
这里要注意,依旧使用下标为0,因为底层使用的是ArrayList,如果采用1,有可能会导致数组溢出问题
class Solution {
public List<List<Integer>> generate(int numRows) {
/**
分析:
初始值为1,每个数是它左上方和右上方的数的和.
将问题抽象一下,找到规律,杨辉三角的第一列和最后一列都是1,中间的数是上一层的的j-1和j列的和。
*/
List<List<Integer>> res = new ArrayList<>();
for(int i = 0; i < numRows; i++){
// 这是中间媒介
List<Integer> temp = new ArrayList<>();
for(int j = 0; j <= i; j++){
// 杨辉三角的第一列( j== 0)和最后一列(i == j)都是1
if(j == 0 || i == j){
temp.add(1);
}else{
// 否则就是去寻找上一层的j-1和j列的和
// 获取上一层信息
List<Integer> prev = res.get( i - 1);
// 计算和
temp.add(prev.get( j - 1) + prev.get(j));
}
}
// 添加到res
res.add(temp);
}
return res;
}
}
119. 杨辉三角 II
注意:使用O(rowIndex) 空间复杂度,是用到了杨辉三角的组合数公式,个人觉得没有必要掌握,所以不记录该题解。
class Solution {
public List<Integer> getRow(int rowIndex) {
/**
杨辉三角的变种题,实际上还是培养抽象思维.
如何保存上一层的信息? 这里使用了更新机制
*/
List<Integer> res = new ArrayList<>();
for(int i = 0; i <= rowIndex; i++){
List<Integer> temp = new ArrayList<>();
for(int j = 0; j <= i; j++){
if(j == 0 || j == i){
temp.add(1);
}else{
// 定位到上一层
temp.add(res.get(j - 1) + res.get(j));
}
}
// 更新
res = temp;
}
return res;
}
}
498. 对角线遍历(边界处理)
注意:第一次解答本题的时候,思路已经正确了,但就是边界条件一直判断不出来,包括后面的调试,也是一直出错(if判断语句应该在循环体外!)
class Solution {
public int[] findDiagonalOrder(int[][] mat) {
/**
分析:
对角线的数学特性是行下标+列下标是该层遍历的数字.
层数遍历的数字范围为[0,m+n-2],层数下标姑且认为是row+col,若row+col是偶数,则右上遍历
若row+col是奇数,则左下遍历。
那么问题就换成右上遍历和左上遍历的边界角标问题了
*/
int m = mat.length;
int n = mat[0].length;
int[] res = new int[m*n];
int row = 0, col = 0;
int index = 0;
// 遍历m+n-1次
for(int i = 0; i < m+n-1; i++){
if(i % 2 == 0){
// 右上遍历
while(row >=0 && col <= n - 1){
// 添加到数组
res[index++] = mat[row][col];
// 遍历在正常范围内,则row-1,col+1
row--;
col++;
}
// 发现 row越界了
if( col <= n-1){
// 修复row
row++;
}else{
// 发现col越界了,修复col
row += 2;
col--;
}
}else{
// 左下遍历
while(row <= m - 1 && col >= 0){
// 添加到数组
res[index++] = mat[row][col];
// 遍历在正常范围内,则row+1,col-1
row++;
col--;
}
// 发现 col越界了
if( row <= m - 1){
// 修复col
col++;
}else{
// 发现row越界了,修复row
row--;
col += 2;
}
}
}
return res;
}
}
12. 整数转罗马数字(使用数组进行枚举)
注意:第一次思路想错了,只枚举题目给的了特殊的六种情况。实际上,题目给的数字全是特殊情况,结果集就是由这些特殊情况所组合起来的,对于数字,要进行排序,然后依次判断。
class Solution {
public String intToRoman(int num) {
// 把阿拉伯数字与罗马数字可能出现的所有情况和对应关系,放在两个数组中
int[] nums = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
String[] romans = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
StringBuilder res = new StringBuilder();
int index = 0;
while (index < 13) {
while (nums[index] <= num) {
res.append(romans[index]);
num -= nums[index];
}
index++;
}
return res.toString();
}
}
13. 罗马数字转整数(哈希表+数组枚举)
注意:第一种解法使用使用了哈希表,来进行累计计算的。
class Solution {
public int romanToInt(String s) {
// 思路和上一题一样,用数组储存特殊情况
// 但是这种情况下无法进行数字的累加组合,比如我扫描到了一个C,实际上有可能会是CM和CD的数字
// 所以这种数组对应的解法在本题不适用。 真的不适用嘛???我把顺序调整为从小到大!
// int[] nums = new int[]{1000,900,500,400,100,90,50,40,10,9,5,4,1};
// String[] romans = new String[]{"M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"};
// int sum = 0;
/**
变换思路:
把所有字母对应的数字(从小到大)添加到哈希表中,扫描一个就进行计算(如果前一个数比后一个数小做减法,否则做加法),这样就完美的进行了运算功能。
*/
Map<Character,Integer> map = new HashMap<>();
map.put('I',1);
map.put('V',5);
map.put('X',10);
map.put('L',50);
map.put('C',100);
map.put('D',500);
map.put('M',1000);
int len = s.length();
int sum = 0;
for(int i = 0; i < len ; i++){
// 获取前一个值
int prev = map.get(s.charAt(i));
// 注意边界以及判断前后数字大小关系
if(i < len - 1 && prev < map.get(s.charAt(i+1))){
// 减法运算
sum -= prev;
}else{
// 加法运算
sum += prev;
}
}
return sum;
}
}