数组:存储一组相同数据类型的数据的集合。
注意事项: 在 Java 中, 数组中包含的变量必须是 相同类型.
数组的使用方法无非就是四个步骤:声明数组、分配空间、赋值、处理。
声明数组:就是告诉计算机数组的类型是什么。有两种形式:int[] array、int array[]。
分配空间:告诉计算机需要给该数组分配多少连续的空间,记住是连续的。array = new int[10];
赋值:赋值就是在已经分配的空间里面放入数据。array[0] = 1 、array[1] = 2……其实分配空间和赋值是一起进行的,也就是完成数组的初始化。有如下三种形式:
基本语法:
// 动态初始化
数据类型[] 数组名称 = new 数据类型 [] { 初始化数据 };
// 静态初始化
数据类型[] 数组名称 = { 初始化数据 };
代码示例:
int[] arr = new int[]{1, 2, 3};
int[] arr = {1, 2, 3};
int arr[] = {1, 2, 3};//和 C语言更相似了.但是我们还是更推荐写成 int[] arr 的形式. int和 [] 是一个整体.
int[] array = {1,2,3,4,5,6,7};//定一个数组并初始化
int[] array2 = new int[3];//代表我们创建了一个可以存放3个整形的数组 默认值为全0
int[] array2 = new int[]{1,2,3,4,5};//代表我们创建了一个可以存放5个整形的数组 并初始化为1 2 3 4 5
每一个软件都占用一定的内存空间。
栈与堆
java 内存分配
int变量例子
int等类型的变量无new关键字,不在堆中开辟空间,值直接在栈中赋给变量名。
数组的初始化则涉及到在堆内存中开辟新的空间(静态初始化的简写,eg:int[] array = {1,2,3},是省略了new int[]的,因此静态初始化也涉及开辟新的空间)。因此实际传递给变量名的实际上是在堆内存中的地址,而非是直接的数值
。
没有通过索引赋值,而是直接将arr1中记录的地址传递给了arr2,那么两个array指向的就是同一个堆中的空间。此时其中一个数组对内存中的值做了更新,那么通过另一个变量名进行访问的时候,得到的也是更新了的值。
为什么数组的题目特别多呢,因为很多题目本质就是查找问题,而数组是查找的最佳载体。很多复杂的算法都是为了提高查找效率的,例如二分查找、二叉树、红黑树、B+树、Hash和堆等等。另一方面很多算法问题本质上都是查找问题,例如滑动窗口问题、回溯问题、动态规划问题等等都是在寻找那个目标结果。
这里只写最简单的方式,根据值是否相等进行线性查找,基本实现如下:
/**
* 顺序查找
* 返回指定元素首次出现的下标位置
*/
public static int sequentialSearch01(int[] arr,int value){
for (int i = 0; i < arr.length; i++) {
if(arr[i] == value){
return i;
}
}
return -1;
}
/**
* 顺序查找
* 返回指定元素出现的下标位置的集合
*/
public static List<Integer> sequentialSearch02(int[] arr,int value){
List<Integer> list = new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
if(arr[i] == value){
list.adds(i);
}
}
return list;
}
在这个修改后的算法中,我们首先检查 size
是否大于或等于原数组 arr
的长度。如果是,则返回 -1
表示操作失败。
否则,我们需要找到要插入的位置 index
,即比要插入的元素 element
大的第一个元素所在的位置。我们通过遍历原数组来寻找该位置。
然后,我们将 index
之后的元素往后移动一位,为要插入的元素腾出空间。
最后,我们将要插入的元素 element
放入 index
的位置,并返回 index
作为添加元素后的下标位置。
/**
* 根据元素大小顺序增加元素到数组中
*
* @param arr 原数组
* @param size 原数组长度
* @param element 要添加的元素
* @return 添加元素后的下标位置,如果操作失败则返回 -1
*/
public static int addByElementSequence(int[] arr, int size, int element) {
if (size >= arr.length){
return -1;
}
int index = size;
for (int i=0; i<size; i++){
if (element < arr[i]){
index = i;
break;
}
}
for (int j = size; j > index; j--){
arr[j] = arr[j - 1];
}
arr[index] = element;
return index;
}
当插入元素时,需要考虑边界情况。在给定的代码中,有两个边界需要特别注意:
size
大于或等于原数组 arr
的长度,表示数组已经达到了最大容量,无法再添加新的元素。此时,我们返回 -1
表示操作失败。index
时,我们遍历原数组,并比较每个元素与要插入的元素的大小。如果找到第一个比要插入元素大的元素,则将 index
设置为该元素的位置。这样,我们可以确保插入元素后的数组仍然保持升序排序。请注意:
如果原数组中的所有元素都小于要插入的元素,则 index
的值将保持为 size
,表示要插入的元素应该放在数组的末尾。
在进行数组元素的移动时,我们从尾部开始向前遍历数组,并将每个元素移动到下一个位置。这样做可以确保不会覆盖之前的元素,同时为要插入的元素腾出空间。最后,我们将要插入的元素放置在正确的位置 index
上。
然后将上面的代码进行优化一下,可以边遍历边识别,这样效率更高
public class ArrayUtils {
/**
* 根据元素大小顺序增加元素到数组中
*
* @param arr 原数组
* @param size 原数组长度
* @param element 要添加的元素
* @return 添加元素后的下标位置,如果操作失败则返回 -1
*/
public static int addByElementSequence(int[] arr, int size, int element) {
if (size >= arr.length) {
return -1;
}
int index = size;
while (index > 0 && element < arr[index - 1]) {
arr[index] = arr[index - 1];
index--;
}
arr[index] = element;
return index;
}
}
在这个修改后的算法中,
我们从 size
的位置开始向前遍历数组。
如果要插入的元素 element
小于当前遍历的元素,则将当前元素往后移动一位,并继续向前遍历。这样,我们可以在找到插入位置之前就完成元素的移动和对比查找。
最后,我们将要插入的元素 element
放入正确的位置 index
上,并返回 index
作为添加元素后的下标位置。
通过从后往前的遍历和移动方式,我们确实可以减少一次对数组的遍历,进一步提高了算法的效率。
对于删除,就不能一边从后向前移动一边查找了,因为元素可能不存在。所以要分为两个步骤,先查是否存在元素,存在再删除
这个方法电荷增加元素一样,必须自己亲自写才有作用,该方法同样要求删除序列最前、中间、最后和不存在的元素都能有效。
public class ArrayUtils {
/**
* 通过元素值删除数组中的元素
*
* @param arr 原数组
* @param size 原数组长度
* @param key 要删除的元素值
* @return 删除元素后的数组长度
*/
public static int removeByElement(int[] arr, int size, int key) {
int index = -1;
for (int i = 0; i < size; i++) {
if (arr[i] == key) {
index = i;
break;
}
}
if (index != -1) {
for (int i = index + 1; i < size; i++) {
arr[i - 1] = arr[i];
}
size--;
}
return size;
}
}
key
的索引位置 index
。size
,使其减少1。需要注意的是,如果要删除的元素在数组中有多个相同的值,这个方法只会删除第一个出现的匹配项。如果需要删除多个匹配项,可以使用循环来重复调用这个方法。
Leetcode896-单调数组
这题要理解什么是单调递增,什么是单调递减,还有[1,1,1,1,1]这种是即属于单调递增,也属于单调递减
class Solution {
public static boolean isMonotonic(int[] nums) {
return isSorted(nums, true) || isSorted(nums, false);
}
public static boolean isSorted(int[] nums, boolean increasing) {
int n = nums.length;
for (int i = 0; i < n - 1; ++i) {
if (increasing) {
if (nums[i] > nums[i + 1]) {
return false;
}
} else {
if (nums[i] < nums[i + 1]) {
return false;
}
}
}
return true;
}
}
首先假设为单调递增,然后去判断,如果是真,则返回true,否则为false
再次假设判断是否为单调递减,如果是真,则返回true,否则为false
那么就会有四种可能:true | true = true(如果数组是这种[1,1,1,1]), true | false = true, false | true = true, false | false = false
所以当既不是递增和递减,就是false,所以符合题目的解法
复杂度如下:
isMonotonic
方法中,调用了两次 isSorted
方法。而 isSorted
方法的时间复杂度是 O(n),其中 n 是数组的长度。因此,整个算法的时间复杂度是 O(n)。public class ArrayUtils {
/**
* 判断数组是否为单调递增或单调递减的
*
* @param nums 给定的整型数组
* @return 如果数组为单调递增或单调递减的,返回 true;否则返回 false
*/
public static boolean isMonotonic_2(int[] nums) {
boolean inc = true, dec = true;
int n = nums.length;
for (int i = 0; i < n - 1; ++i) {
if (nums[i] > nums[i + 1]) {
inc = false;
}
if (nums[i] < nums[i + 1]) {
dec = false;
}
}
return inc || dec;
}
}
时间复杂度仍然是 O(n),因为我们需要遍历整个数组。空间复杂度是 O(1),因为只使用了常量级别的额外空间。
88. 合并两个有序数组
/**
* 方法1:先合并再排序实现排序
*
* @param nums1 第一个数组
* @param nums1_len 第一个数组的长度
* @param nums2 第二个数组,将nums2合并到nums1中
* @param nums2_len 第二个数组的长度
*/
public static void merge1(int[] nums1, int m, int[] nums2, int n) {
for (int i = 0; i < m; ++i) {
nums1[m + i] = nums2[i];
}
Arrays.sort(nums1);
}
空间复杂度为O(1),因为它只使用了常数级别的额外空间来存储变量和索引。
时间复杂度约为O(nums2_len + (nums1_len + nums2_len)log(nums1_len + nums2_len))。
首先,我们定义三个指针:i
、len1
和len2
。其中,i
指向nums1
的末尾,len1
指向nums1
中的最后一个元素,len2
指向nums2
中的最后一个元素。
然后,我们使用一个循环来比较nums1
和nums2
的元素,并将较大的元素放入nums1
的末尾。具体的比较逻辑如下:
nums1[len1] <= nums2[len2]
,表示nums2
中的当前元素较大或两个元素相等。此时,将nums2[len2]
放入nums1[i]
,然后i
和len2
都向前移动一位。nums1[len1] > nums2[len2]
,表示nums1
中的当前元素较大。此时,将nums1[len1]
放入nums1[i]
,然后i
和len1
都向前移动一位。上述步骤会一直重复,直到其中一个数组的所有元素都被遍历完毕。
如果存在某个数组还有剩余元素,意味着剩下的元素都比已经合并的部分的元素要小。因此,我们只需将剩余的元素按照顺序放入nums1
的前面即可。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m + n - 1;
int len1 = m - 1, len2 = n - 1;
while (len1 >= 0 && len2 >= 0) {
if (nums1[len1] <= nums2[len2])
nums1[i--] = nums2[len2--];
else if (nums1[len1] > nums2[len2])
nums1[i--] = nums1[len1--];
}
//假如A或者B数组还有剩余
while (len2 != -1) nums1[i--] = nums2[len2--];
while (len1 != -1) nums1[i--] = nums1[len1--];
}
}
空间复杂度为O(1),与之前的优化方法相同。
时间复杂度为O(nums1_len + nums2_len)
这里的优化只是一个微调,当其中一个数组已经被遍历完毕后,我们可以提前结束循环,而不需要再继续比较剩下的元素。这样可以减少一些不必要的比较操作。
另外,在将剩余的元素放入nums1
之前,我们可以先检查一下nums1
和nums2
中剩余元素的情况。如果nums1
中还有未处理的元素,那么它们已经在正确的位置上,不需要再移动。只需要将nums2
中剩下的元素按顺序放入nums1
的前面即可。同样地,如果nums2
中还有未处理的元素,将它们直接放到nums1
的前面即可。
public static void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m + n - 1;
int len1 = m - 1, len2 = n - 1;
while (len1 >= 0 && len2 >= 0) {
if (nums1[len1] <= nums2[len2]) {
nums1[i--] = nums2[len2--];
} else {
nums1[i--] = nums1[len1--];
}
}
while (len2 >= 0) {
nums1[i--] = nums2[len2--];
}
}
这个优化后的方法可以更早地结束循环,并且对剩余元素的处理更加简洁和高效。
它仍然具有O(nums1_len + nums2_len)的时间复杂度和O(1)的空间复杂度。