数组理论基础
数组是存放在连续内存空间上的相同类型数据的集合。
需要两点注意的是
-
数组下标都是从0开始的。
-
数组内存空间的地址是连续的
正是因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
但是数组在不同语言中的内存地址是否连续取决于语言本身:
对于C++来说:
void test_arr() {
int array[2][3] = {
{0, 1, 2},
{3, 4, 5}
};
cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl;
cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl;
}
int main() {
test_arr();
}
有如下运行地址:
0x7ffee4065820 0x7ffee4065824 0x7ffee4065828
0x7ffee406582c 0x7ffee4065830 0x7ffee4065834
而在Java中:
public static void test_arr() {
int[][] arr = {{1, 2, 3}, {3, 4, 5}, {6, 7, 8}, {9,9,9}};
System.out.println(arr[0]);
System.out.println(arr[1]);
System.out.println(arr[2]);
System.out.println(arr[3]);
}
有如下运行地址:
[I@7852e922
[I@4e25154f
[I@70dea4e
[I@5c647e05
基于此我总结了一下我们常见的一些语言:
二维数组在内存中连续存储的语言
- C语言:与C++类似,C语言中的二维数组在内存中也是连续存储的。例如声明一个二维数组
int arr[3][4]
,其在内存中会按照arr[0][0]、arr[0][1]、arr[0][2]、arr[0][3]、arr[1][0]、arr[1][1]……
的顺序连续存放,相邻元素的地址差值与数组元素类型所占字节大小有关。 - C++语言:如你提供的测试代码和结果所示,在C++中二维数组是连续分布的,其元素在内存中按照声明的顺序依次排列,相邻元素的内存地址是连续的,且地址差值与数组元素类型所占字节大小相关。
- Fortran语言:Fortran中的二维数组在内存中也是连续存储的,并且采用列优先存储方式,即先存储列元素,再存储行元素。例如一个二维数组
arr(3,4)
,其在内存中会按照arr(1,1)、arr(2,1)、arr(3,1)、arr(1,2)、arr(2,2)……
的顺序连续存放。 - C#语言:C#中的二维数组在内存中是连续存储的。例如声明一个二维数组
int[,] arr = new int[3,4]
,其元素在内存中会按照一定的顺序连续存放,相邻元素的内存地址是连续的。 - Go语言:Go语言中的数组是值类型,二维数组在内存中也是连续存储的。当声明一个二维数组时,编译器会为其分配一块连续的内存区域来存储所有元素。
二维数组在内存中不连续存储的语言
- Java语言:如你提供的测试代码和结果所示,Java中的二维数组并不是连续存储的。Java的二维数组实际上是一个数组的数组,每个一维数组(即二维数组的每一行)是独立的对象,它们在堆内存中的存储位置是分散的,没有固定的顺序和连续性。通过输出的地址也可以看出,二维数组的每一行头结点的地址是没有规则的。
- Python语言:Python中的二维数组(列表的列表)在内存中也是不连续存储的。Python的列表是动态数组,其内部存储的是对象的引用,二维数组中的每个一维列表(即二维数组的每一行)是独立的对象,这些对象在内存中的存储位置是分散的。例如
arr = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
,arr
中的每个子列表在内存中的位置是不连续的。 - JavaScript语言:JavaScript中的二维数组也是不连续存储的。JavaScript的数组是一种特殊的对象,二维数组中的每个一维数组(即二维数组的每一行)是独立的对象,它们在内存中的存储位置是分散的,通过索引访问元素时,会根据哈希表的方式查找对应的内存地址。
- Ruby语言:Ruby中的二维数组是数组的数组,每个一维数组是独立的对象,在内存中的存储位置是分散的,没有固定的顺序和连续性。
- PHP语言:PHP中的二维数组也是不连续存储的。PHP的数组本质上是有序映射,二维数组中的每个一维数组是独立的映射对象,它们在内存中的存储位置是分散的。
704二分查找
左闭右开区间
[left, right)
,初始时right
为数组长度,表示开区间。循环条件是left < right
,在更新边界时,如果中间元素小于目标值,左边界更新为mid + 1
;如果中间元素大于目标值,右边界更新为mid
,因为mid
位置的元素已经检查过,且右边界是开区间,不包括mid
。
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return -1;
}
左闭右闭区间
[left, right]
,初始时right
为数组最后一个元素的索引,表示闭区间。循环条件是left <= right
,在更新边界时,如果中间元素小于目标值,左边界更新为mid + 1
;如果中间元素大于目标值,右边界更新为mid - 1
,因为mid
位置的元素已经检查过,且右边界是闭区间,需要排除mid
。
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 右边界为数组最后一个元素的索引,表示闭区间
while (left <= right) { // 当左边界小于等于右边界时继续循环
int mid = left + (right - left) / 2; // 计算中间位置
if (nums[mid] == target) { // 如果中间元素等于目标值
return mid; // 返回中间位置的索引
} else if (nums[mid] < target) { // 如果中间元素小于目标值
left = mid + 1; // 更新左边界为中间位置的下一个位置
} else { // 如果中间元素大于目标值
right = mid - 1; // 更新右边界为中间位置的前一个位置,因为是闭区间,所以不包括mid
}
}
return -1; // 如果循环结束仍未找到目标值,返回-1
}
35. 搜索插入位置
左闭右开区间
[left, right)
,初始时right
为数组长度,表示开区间。循环条件是left < right
,在更新边界时,如果中间元素小于目标值,左边界更新为mid + 1
;如果中间元素大于目标值,右边界更新为mid
。最终返回left
,因为left
会指向目标值应该插入的位置。
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
左闭右闭区间
[left, right]
,初始时right
为数组最后一个元素的索引,表示闭区间。循环条件是left <= right
,在更新边界时,如果中间元素小于目标值,左边界更新为mid + 1
;如果中间元素大于目标值,右边界更新为mid - 1
。最终返回left
,因为left
会指向目标值应该插入的位置。
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
27.移除元素
单循环暴力解法
通过一个循环遍历数组,遇到不等于val
的元素就将其移动到数组前面,同时记录不等于val
的元素数量。这种方法简单易懂,但可能需要移动很多元素。
public int removeElement(int[] nums, int val) {
int k = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] != val) {
nums[k] = nums[i];
k++;
}
}
return k;
}
双指针解法
使用两个指针,左指针指向不等于val
的元素,右指针遍历数组。当右指针遇到不等于val
的素时,将其移动到左指针位置,同时左指针右移。这种方法可以减少元素的移动次数,提高效率。
public int removeElement(int[] nums, int val) {
int left = 0;
int right = 0;
while (right < nums.length) {
if (nums[right] != val) {
nums[left] = nums[right];
left++;
}
right++;
}
return left;
}