以上就是数组数据结构的一些理论要点,接下来进行算法训练。
算法训练
《剑指offer》关于数组的算法训练共有3题:构建乘积数组【难度2】、数组中重复的数字【难度3】、二维数组中的查找【难度4】。
构建乘积数组
给定一个数组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]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * … * A[n-1],B[n-1] = A[0] * A[1] * … * A[n-2];)
分析
这道题比较好理解,当然对于重新捡起来我还是有些难度,题的意思很明确,就是长度为n的A、B两个数组,B的每个元素都是除了A中这个元素的其它元素的连乘:
- A、B的长度是相同的,都是n
- B的每个元素都是除了A中这个元素的其它元素的连乘
- 不能用除法
- 边界值:数组不存在、数组长度小于2均返回false,不满足边界条件
这样就是这个题的四要素,然后得出如下的图。
解法
其实最直观的解法就是把A的元素连乘,然后循环,每个B的元素等于A元素连乘的结果除以当前B元素索引位置A的值。但是这样与不能用除法相悖。所以还有第二直观的方法,就是左边获取一个乘积,右边获取一个乘积,再相乘,但这样的算法复杂度却比较高
import java.util.ArrayList; public class Solution { public int[] multiply(int[] A) { //设定边界条件,B[n-1] = A[0] * A[1] * ... * A[n-2];所以A长度必须大于等于2,否则会报索引越界 if(A==null||A.length<=1) return null; //动态初始化数组B int length = A.length; int[] B=new int[length]; //循环给B赋值,得到下三角 for(int i=0;i<length;i++){ int temp1=1; //用temp1接当前元素左边的连乘积 int temp2=1; //用temp2接当前元素右边的连乘积 //用temp1接当前元素左边的连乘积 for(int j=0;j<i;++j){ temp1*=A[j]; } //用temp2接当前元素右边的连乘积 for(int m=i+1;m<length;++m){ temp2*=A[m]; } B[i]=temp1*temp2; } return B; } }
其实可以从图中看出,B数组可以看做两个倒三角的乘积。
import java.util.ArrayList; public class Solution { public int[] multiply(int[] A) { //设定边界条件,B[n-1] = A[0] * A[1] * ... * A[n-2];所以A长度必须大于等于2,否则会报索引越界 if(A==null||A.length<=1) return null; //动态初始化数组B int length = A.length; int[] B=new int[length]; //循环给B赋值,得到上三角 B[0]=1; for(int i=1;i<length;i++){ B[i]=B[i-1]*A[i-1]; } //循环给B赋值,乘以下三角 int temp=1; for(int i=length-2;i>=0;i--){ temp*=A[i+1]; B[i]*=temp; } return B; } }
这道题的收获是:1,要将要求转换为分解条件,最好有图解。2,注意边界条件
数组中重复的数字
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
分析
按照之前的训练思维来处理,先分解题目信息:
- 数组长度为n,整数数组
- 数组中有重复数字,重复数字的个数为0到n/2之间
- 找到第一个重复的数字
- 边界值:数组不存在、数组长度为0均返回false,不满足边界条件
这样就是这个题的四要素,接下来按照要求来做一下题。
解法
自己AC了一种复杂度比较高但比较直观的解法,就是构造一个数组B用来存储历史值, //如果当前值在历史值中存在则命中,否则加入历史值。
public class Solution { public boolean duplicate(int numbers[],int length,int [] duplication) { //如果数组不存在或者只有一个数字,则直接返回false; if(numbers==null||length<=1) return false; int[] B=new int[length]; B[0]=numbers[0]; //用B存储左边的值 for(int i=1;i<length;i++){ for(int j=0;j<i;j++){ //如果当前值在历史值中存在则命中,否则加入历史值 if(numbers[i]==B[j]){ duplication[0]=numbers[i]; return true; }else{ B[i]=numbers[i]; } } } return false; } }
其实还有一种更好的解法,就是构造一个标布尔数组,因为numbers里存的都是0到n-1整数,所以其实可以当做另一个数组的索引下标。
public class Solution { //boolean只占一位,所以还是比较省的 public boolean duplicate(int numbers[], int length, int[] duplication) { //构造一个二进制数组 boolean[] k = new boolean[length]; for (int i = 0; i < k.length; i++) { //把数组值当做这个二进制数组的索引,只要发现该索引位被标记过,就认为重复 if (k[numbers[i]] == true) { duplication[0] = numbers[i]; return true; } //设置二进制数组的索引位数组值为true k[numbers[i]] = true; } return false; } }
这道题的收获是:思路要灵活,注意题中的0–n-1,思考为什么这么设置。
二维数组中的查找
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
分析
按照之前的训练思维来处理,先分解题目信息:
- 数组为一个n*n的二维数组,说白了就是一个正方形数组。
- 数组的每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序
- 判断数组中是否存在一个整数。
- 边界值:数组不存在、数组长度为0、数组行长度为0,均返回false,不满足边界条件
这样就是这个题的三要素,接下来按照要求来做一下题。画个模型来辅助理解。例如我们要命中下面矩阵中的6.
解法
自己AC了一种复杂度比较高但比较直观的解法,就是遍历二维数组寻找整数值,这种做法当然不可取,复杂度高也浪费了顺序递增的条件。
public class Solution { public boolean Find(int target, int [][] array) { //如果数组不存在或者长度为0,直接返回false if(array==null||array.length<1||array[0].length<1) return false; for(int i=0;i<array.length;i++){ for(int j=0;j<array.length;j++){ if(target==array[i][j]) return true; } } return false; } }
还有一种用上这两种条件的解法,经过简单思考就能想到,利用其顺序性,逐步缩小范围
public class Solution { public boolean Find(int target, int [][] array) { //如果数组不存在或者长度为0,直接返回false if(array==null||array.length<1||array[0].length<1) return false; //声明行、列以及检索下标 int rows=array.length; int columns=array.length; int column=columns-1; int row=0; //移动光标的过程中分别做三次判断 while(row<rows&&column>=0){ if(target==array[row][column]) return true; if(target>array[row][column]) row++; else column--; } return false; } }
这道题的收获是:思路要灵活,注意题中每行每列的顺序性,还有就是在矩阵二维数组中,多考虑下指针移动的过程。
思考
其实数组算是一种比较基础的数据结构,而且一般不会遇到太复杂的数组结构,一般也就到二维,更高维的维度计算也比较少。本次训练收获如下:
- 拆解:遇到问题一定要把题拆解开来,拆解成一系列的条件
- 画图:拆解完成之后,最好依据题目描述构造一个简单的Demo来辅助理解题目的意思
- 边界值:写代码的时候一定要注意边界值,防止溢出
- 特殊说明:要注意题目中的特殊说明,这些特殊说明可以帮助你跳出常规的复杂计算方式
思维也会因算法训练更加严谨。