数组学习内容
数组
1. 数组理论基础
数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力
也就是说,想法很简单,但实现起来 可能就不是那么回事了。
首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题
数组是存放在连续内存空间上的相同类型数据的集合。
数组可以方便的通过下标索引的方式获取到下标下对应的数据。
举一个字符数组的例子,如图所示:

需要两点注意的是
- 数组下标都是从0开始的。
- 数组内存空间的地址是连续的
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:

而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。
数组的元素是不能删的,只能覆盖。
那么二维数组直接上图,大家应该就知道怎么回事了

那么二维数组在内存的空间地址是连续的么?
不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的。
我们来做一个实验,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
注意地址为16进制,可以看出二维数组地址是连续一条线的。
一些同学可能看不懂内存地址,我就简单介绍一下, 0x7ffee4065820 与 0x7ffee4065824 差了一个4,就是4个字节,因为这是一个int型的数组,所以两个相邻数组元素地址差4个字节。
0x7ffee4065828 与 0x7ffee406582c 也是差了4个字节,在16进制里8 + 4 = c,c就是12。
如图:

所以可以看出在C++中二维数组在地址空间上是连续的。
2. 二分法
2.1 二分法简介
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
运行时间为O(longn)。
2.2 二分法实现
二分法查找中需要注意右侧right值:
- r i g h t = l e n − 1 right = len - 1 right=len−1
- r i g h t = l e n right = len right=len
第一种方法:
在数组:1,2,3,4,7,9,10中查找元素2,如图所示:

代码:
// 版本一
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
第二种方案:

在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(注意和方法一的区别)
代码:
// 版本二
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在[middle + 1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
个人注意
2.3 Leetcode相关题目
704 二分法查找
代码见上
class Solution{
public:
int searchInsert(vector<int>& nums, int target){
int left = 0, right = nums.size()-1;
// 与正常二分法相同
while (left <= right)
{
int mid = left + (right - left) / 2;
if (target == nums[mid])
return mid;
else if (target > nums[mid])
left = mid + 1;
else
right = mid - 1;
}
return right;
// 二分法区间区分趋近于左边,如果未找到相同的值,
// 那么最后一次while循环中,left和right一定在该值的左边,left+1后循环结束,right即为当前数字插入位置。
}
};
class Solution{
public:
// 找到左边界,注意target == nums[mid]情况改动
int left_bound(vector<int> nums, int target){
int left = 0, right = nums.size() - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (target == nums[mid])
right = mid - 1; # 使右指针向左移动,继续查找结果
else if (target > nums[mid])
left = mid + 1;
else
right = mid - 1;
}
//判断边界条件
if (left >= nums.size() || target != nums[left])
return -1;
return left;
}
// 找到右边界,注意target == nums[mid]情况改动
int left_bound(vector<int> nums, int target){
int left = 0, right = nums.size() - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if (target == nums[mid])
left = mid + 1; // 使左指针向右移动,继续查找结果
else if (target > nums[mid])
left = mid + 1;
else
right = mid - 1;
}
}
//判断边界条件
if (right < 0 || target != nums[right])
return -1;
return right;
}
vector<int> searchRange(vector<int> nums, int target){
int left = left_bound(nums, target);
if (left == -1) return {-1, -1};
int right = right_bound(nums, target);
return {left, right};
}
};
69.x 的平方根
class Soltion{
public:
int Mysqrt(int x){
int left = 0, right = x;
while (left <= right){
long mid = left + (right - left) / 2; // 定义长整型,防止乘法时数字溢出
if (x == mid*mid)
return mid;
else if (x > mid*mid)
left = mid + 1;
else
right = mid - 1;
}
return right;
}
};
367.有效的完全平方数
class Solution{
public:
bool isPerfectSquare(int num){
int left = 0, right = num;
while (left <= right)
{
long mid = left + (right - left) / 2;
if (num == mid*mid)
return true;
else if (num > mid*mid)
left = mid + 1;
else
right = mid - 1;
}
return false;
}
};
3. 移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。
示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
你不需要考虑数组中超出新长度后面的元素
3.1 思路:
有的同学可能说了,多余的元素,删掉不就得了。
要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。
数组的基础知识可以看这里程序员算法面试中,必须掌握的数组理论知识 (opens new window)。
暴力解法(不想多解释,看看热闹就完了)
这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。
删除过程如下:
在这里插入图片描述

- 时间复杂度: O ( n 2 ) O(n^2) O(n2) 劝退
- 空间复杂度: O ( 1 ) O(1) O(1)
双指针法
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
// 时间复杂度:O(n)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (val != nums[fastIndex]) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
};
注意这些实现方法并没有改变元素的相对位置!
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( 1 ) O(1) O(1)
3.2 相关题目
27移除元素
class Solution{
public:
int removeElement(vector<int>& nums, int val){
int slow = 0;
for (int fast = 0; fast < nums.size(); ++fast){
if (nums[fast] != val){
nums[slow++] = nums[fast];
}
}
return slow;
}
};
class Solution{
public:
int removeDuplicates(vector<int>& nums, int target){
if (nums.size() <= 0) return 0;
int slow = 1;
for (int fast = 1; fast < nums.size(); ++fast){
if (nums[fast-1] != nums[fast])
nums[slow++] = nums[fast];
++fast;
}
return slow;
}
};
283 移动零
class Solution{
public:
void moveZero(vector<int>& nums){
int slow = 0;
for (int fast = 0; fast < nums.size(); ++fast){
if (nums[fast] != 0)
swap(nums[slow++], nums[fast]);
++fast;
}
}
};
class Solution{
public:
bool backspaceCompare(string s, string t){
int l1 = s.length() - 1, skip1 = 0; // 从最后开始考虑
int l2 = t.length() - 1, skip2 = 0;
//深刻理解||关系,当两者去掉#后的长度不同时,我们需要继续进行循环比较,否则,存在相同长度内字母相同,但不同长度内,字母不同现象。
while (l1 >= 0 || l2 >= 0){
// s找到有意义的数
while (l1 >= 0){
if (s[l1] == '#'){
++skip1, --l1;
}
else if (skip1 > 0){
--skip1, --l1;
}
else
break;
}
// t找到有意义的数
while (l2 >= 0){
if (s[l2] == '#'){
++skip2, --l2;
}
else if (skip2 > 0){
--skip2, --l2;
}
else
break;
}
if (l1 >= 0 && l2 >= 0){
if (s[l1] != t[l2])
return false;
}
else{
if (l1 >= 0 || l2 >= 0) // 保证两个数同时满足
return false;
}
--l1;
--l2;
}
return true;
}
};
4 有序数组的平方
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1: 输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100]
示例 2: 输入:nums = [-7,-3,2,3,11] 输出:[4,9,9,49,121]
4.1 思路
暴力解法
最直观的相反,莫过于:每个数平方之后,排个序,美滋滋,代码如下:
class Solution{
public:
vector<int> sortedSquares(vector<int>& nums) {
for (int i = 0, i < nums.size(); ++i){
nums[i] *= nums[i];
}
sort(nums.begin(), nums.end()); //快速排序
return nums;
}
};
- 时间复杂度 O(n + nlongn)
- 空间复杂度 O(1)
class Solution{
public:
vector<int> sortSquares(vector<int>& nums){
int left = 0, right = nums.size() - 1;
vector<int> p(nums.size()); // 预留vector大小
int index = nums.size() - 1;
while (left <= right){
if (abs(nums[left]) < abs(nums[right])){
p[index--] = nums[right]*nums[right];
--right;
}
else{
p[index--] = nums[left]*nums[left];
++left;
}
}
return p;
}
};
- 时间复杂度 O(n)
- 空间复杂度 O(n)
5 ★长度最小的子数组(滑动窗口解法)
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
5.1 思路
暴力解法
这道题目暴力解法当然是 两个for循环,然后不断的寻找符合条件的子序列,时间复杂度很明显是O(n^2) 。
代码如下:(不建议看)
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX; // 最终的结果
int sum = 0; // 子序列的数值之和
int subLength = 0; // 子序列的长度
for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
sum = 0;
for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
sum += nums[j];
if (sum >= s) { // 一旦发现子序列和超过了s,更新result
subLength = j - i + 1; // 取子序列的长度
result = result < subLength ? result : subLength;
break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
}
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
时间复杂度: O ( n 2 ) O(n^2) O(n2) 空间复杂度: O ( 1 ) O(1) O(1)
★重点
滑动窗口
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:

最后找到 4,3 是最短距离。
其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。
在本题中实现滑动窗口,主要确定如下三点:
窗口内是什么?
如何移动窗口的起始位置?
如何移动窗口的结束位置?
窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。
窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。
解题的关键在于 窗口的起始位置如何移动,如图所示:

可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。
class Solution{
public:
int minSubArrayLen(int target, vector<int>& nums){
int left = 0, right = 0; // 限定左右窗口
int sum = 0; //总和
int subLength = 0; //长度
int result = INT_MAX; //最优结果
while (right <= nums.size()){
sum += nums[right];
// 判断是否满足基础条件,如果满足就收缩窗口
while (sum >= target){
subLength = right - left + 1;
result = result > subLength? subLength: result;
sum -= nums[left];
++left;
}
++right;
}
return result == INT_MAX? 0: result;
}
};
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
一些录友会疑惑为什么时间复杂度是 O ( n ) O(n) O(n)。
不要以为for里放一个while就以为是 O ( n 2 ) O(n^2) O(n2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被被操作两次,所以时间复杂度是2 * n 也就是 O ( n ) O(n) O(n)。
5.2 相关题目
209 长度最小的子数组
代码如上
904 水果成篮
class Solution{
public:
int totalFruit(vector<int>& fruits){
unordered_map<int, int> basket;
int left = 0, right = 0;
int result = 0, subLength = 0;
while (right < fruits.size()){
basket[fruits[right]]++;
++right;
while (basket.size() > 2){
basket[fruits[left]]--;
if (basket[fruits[left]] == 0) basket.erase(fruits[left]);
++left;
}
subLength = right - left;
result = result > subLength? result: subLength;
}
return result;
}
};
★滑动窗口框架
void slidingWindow(string s, string t){
unordered_map<char, int> need, window;
for (char c: t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()){
// c是移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
...
// debug 输出的位置
cout << "window: " << "[" << left << ", " << right << endl;
//判断左侧矿口是否要收缩
while (window needs shrink){
// d是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
//进行窗口内数据的一系列更新
...
}
}
}
76 最小覆盖子串
class Solution{
public:
string minWindow(string s, string t){
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
//记录最小覆盖子串的起始索引及长度
int start = 0, len = INT_MAX;
while (right < s.size()){
char c = s[right];
// c是将移入窗口的字符
right++;
// 进行窗口内数据的一系列更新
if (need.count(c)){
window[c]++;
if (need[c] == window[c])
valid++;
}
// 判断左侧窗口是否要收缩
while (valid == need.size()){
// 在这里更新最小覆盖字串
if (right - left < len){
start = left;
len = right - left;
}
// d是将移出窗口的字符
char d = s[left];
// 左移窗口
++left;
// 进行窗口内数据的一系列更新
if (need.count(d)){
if (need[d] == window[d])
valid--;
window++;
}
}
}
// 返回最小覆盖子串
return len == INT_MAX? "" : s.substr(start, len);
}
};
567 字符串的排列
class Solution {
public:
bool checkInclusion(string s1, string s2) {
unordered_map<char, int> need, window;
for (char c: s1) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s2.size()){
char c = s2[right];
right++;
if (need.count(c)){
window[c]++;
if (window[c] == need[c])
valid++;
}
while (right - left >= s1.size()){
if (valid == need.size()){
return true;
}
char d = s2[left];
++left;
if (need.count(d)){
if (window[d] == need[d])
valid--;
window[d]--;
}
}
}
return false;
}
};
438 找到字符串中所有字母异位词
class Solution{
public:
vector<int> findAnagrams(string s, string p){
unordered_map<char, int> need, window;
for (char c : p) need[c]++;
int left = 0, right = 0;
int valid = 0;
vector<int> res;
while (right < s.size()){
char c = s[right++];
if (need.count(c)){
window[c]++;
if (window[c] == need[c])
valid++;
}
while (right - left >= p.size()){
if (valid == need.size())
res.push_back(left);
char d = s[left++];
if (need.count(d)){
if (need[d] == window[d])
valid--;
window[d]--;
}
}
}
return res;
}
};
class Solution{
public:
int lengthOfLongestSubstring(string s){
unordered_map<char, int> window;
int left = 0, right = 0;
int result = 0;
while (right < s.size()){
char c = s[right++];
window[c]++;
while (window[c] > 1){
char d = s[left++];
window[d]--;
}
result = max(result, right - left);
}
return result;
}
};
6. 螺旋矩阵
给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
输入: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]

6.1 思路
求解本题依然是要坚持循环不变量原则。
模拟顺时针画矩阵的过程:
填充上行从左到右
填充右列从上到下
填充下行从右到左
填充左列从下到上
由外向内一圈一圈这么画下去。
这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开又闭的原则,这样这一圈才能按照统一的规则画下来。
那么我按照左闭右开的原则,来画一圈,大家看一下:

这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。
这也是坚持了每条边左闭右开的原则。
代码如下,已经详细注释了每一步的目的,可以看出while循环里判断的情况是很多的,代码里处理的原则也是统一的左闭右开。
代码:
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 每一圈循环,需要控制每一条边遍历的长度
int i,j;
while (loop --) {
i = startx;
j = starty;
// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j = starty; j < starty + n - offset; j++) {
res[startx][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = startx; i < startx + n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;
// offset 控制每一圈里每一条边遍历的长度
offset += 2;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};
6.2 相关题目
59 螺旋矩阵 II
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n));
int left = 0, right = n - 1, top = 0, bottom = n - 1;
int count = 1;
while(true){
// 从左到右
for (int j = left; j <= right; ++j)
res[left][j] = count++;
if (++top > bottom) break;
// 从上到下
for (int i = top; i <= bottom; ++i)
res[i][right] = count++;
if (--right < left) break;
// 从右到左
for (int j = right; j >= left; --j)
res[bottom][j] = count++;
if (--bottom < top) break;
// 从下到上
for (int i = bottom; i >= top; --i)
res[i][left] = count++;
if (++left > right) break;
}
return res;
}
};
剑指Offer 29. 顺时针打印矩阵

class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.empty() return {};
// 矩阵行列大小
int rows = matrix.size(), columns = matrix[0].size();
vector<int> order;
// 左,右,上,下四个边界
int left = 0, right = columns - 1, top = 0, bottom = rows - 1;
while (true) {
// 从左到右
for (int column = left; column <= right; column++) {
order.push_back(matrix[top][column]);
}
if (++top > bottom) break;
// 从上到下
for (int row = top; row <= bottom; row++) {
order.push_back(matrix[row][right]);
}
if (--right < left) break;
// 从右到左
for (int column = right; column >= left; column--) {
order.push_back(matrix[bottom][column]);
}
if (--bottom < top) break;
// 从下到上
for (int row = bottom; row > top; row--) {
order.push_back(matrix[row][left]);
}
if (++left > right) break;
}
return order;
}
};
总结
数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力
也就是说,想法很简单,但实现起来 可能就不是那么回事了。
首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题
数组是存放在连续内存空间上的相同类型数据的集合。
数组可以方便的通过下标索引的方式获取到下标下对应的数据。
举一个字符数组的例子,如图所示

需要两点注意的是
数组下标都是从0开始的。
数组内存空间的地址是连续的
正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示:

而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。
数组的元素是不能删的,只能覆盖。
那么二维数组直接上图,大家应该就知道怎么回事了

那么二维数组在内存的空间地址是连续的么?
我们来举一个例子,例如: int[][] rating = new int[3][4]; , 这个二维数据在内存空间可不是一个 3*4 的连续地址空间

所以二维数据在内存中不是 3*4 的连续地址空间,而是四条连续的地址空间组成!
数组的经典题目
- 二分法
这道题目呢,考察的数据的基本操作,思路很简单,但是在通过率在简单题里并不高,不要轻敌。
可以使用暴力解法,通过这道题目,如果准求更优的算法,建议试一试用二分法,来解决这道题目
暴力解法时间复杂度:O(n) 二分法时间复杂度:O(logn)
在这道题目中我们讲到了循环不变量原则,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。
二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力。
- 双指针法
双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
暴力解法时间复杂度:O(n^2) 双指针时间复杂度:O(n)
这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为一下两点:
数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。
C++中vector和array的区别一定要弄清楚,vector的底层实现是array,所以vector展现出友好的一些都是因为经过包装了。
双指针法(快慢指针法\左右指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法
- 滑动窗口
暴力解法时间复杂度:O(n^2) 滑动窗口时间复杂度:O(n)
本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。
如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的
- 模拟行为
模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。
在这道题目中,我们再一次介绍到了循环不变量原则,其实这也是写程序中的重要原则。
相信大家又遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,踩了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实真正解决题目的代码都是简洁的,或者有原则性的,大家可以在这道题目中体会到这一点
注:内容参考和更新于代码随想录,主要用于本人学习和使用,由于博客中的代码为本人手敲,未得到有效验证,如有问题可评论或者留言。

本文详细介绍了数组的基础知识,包括内存存储方式、数组操作以及二维数组的特点。重点讲解了二分法的原理与实现,包括在LeetCode中的应用题目。此外,文章还探讨了移除数组元素的双指针法,以及利用滑动窗口解决长度最小子数组问题。最后,展示了如何通过模拟行为解决螺旋矩阵问题,并总结了数组相关面试题的解题技巧。
196

被折叠的 条评论
为什么被折叠?



