学习目标
- 可以理解数组是什么以及它的底层存储原理。
- 会正确的定义数组以及遍历数组。
- 能够在算法与数据结构的习题中熟练使用数组存储数据。
- 不必过多纠结于一些细枝末节的东西,会用就行!!
PS:讲解会用最通俗易懂的方式来写(因为一些复杂的原理细节我也不懂hh~)
会引用一些大佬写的文献或者一些画好的图来辅助我们理解。
什么是数组?
先给出百度百科的解释:
数组(Array)是有序的元素序列。若将有限个类型相同的变量的集合命名,那么这个名称为数组名。
组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。
用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按有序的形式组织起来的一种形式。 这些有序排列的同类数据元素的集合称为数组。
简而言之:
数据就是用来存储数据的,且是用来存储相同类型的数组的,比如整型、字符型等等。每一种编程语言对数组的定义和使用方式又不尽相同,但是整体的思想是一样的,所以大家不必过多去纠结于编程语言的差异性。
存储方式
数组是存放连续存储空间上的相同类型数据的集合。
数组可以方便的通过下标索引的方式获取到下标下对应的数据。
需要注意的是:
- 数组的下标是从0开始的
- 数组内存空间是连续的
- 数组不能删除,只能覆盖
📌由于空间连续,在删除和添加元素的时候会移动其他元素,这一点在题目里常用。
定义方式
一维数组
几种常见的定义举例:
int a[10];
double db[2000];
char str[10000];
bool hashtable[1000];
初始化:
一维数组的初始化,需要给出用逗号隔开的从第一个元素开始的若干个元素的初值,并用大括号括住。
后面未被赋初值的元素将会由不同编译器内部实现的不同而被赋以不同的初值(可能是很大的随机数),而一般情况默认初值为0。如下图:
可以看到没有初始化的数据的的确确被赋值为0了。
二维数组
一个二维数组,在本质上,是一个一维数组的列表。声明一个x行y列的二维整型数组,形式如下:
数据类型 数组名[x][y];
例如定义一个整型的包含3行和4列的二维数组:
int a[3][4];
那么二维数组在内存的空间地址是连续的么? 上实验图!
可以看到地址在内存里是用16进制来表示的,0x6ffe00和0x6ffe04相差4,也就是int 的四个字节。
所以可以看出在C++中二维数组在地址空间上是连续的。
但是并不是在所有编程的语言中都是连续的哦,比如在java中就不是连续的,至于为什么这不是这一篇的重点知识,交给大家自行研究啦~以后如果将java的虚拟机的话可以提一提。😀
vector
是什么?
vector首先是c++自带的stl模板中第一个提到的一个十分好用的库,翻译解释为“向量”。
其实大家可以把它当做一个可变长的数组来使用,什么叫做可变长呢?就是在初始化的时候并不需要考虑它需要有对少个元素,空间是可变的!可以理解为plus版本的数组。
如果要使用vector,则需要添加vector头文件,除此之外,还需要在头文件下面加上一句“using namespace std;”,这样就可以在代码中使用vector了。
#include<vector>
using namespace std;
📌要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。
但是我还是那句话,那年我双手插兜,哦不对,只要会用就行了hh~
所以下面就用我当时学习vector的时候,实操的代码以及注释来讲解!
怎么使用?
#include<bits/stdc++.h>
using namespace std;
/*
vector可以作为可边长数组使用,在不确定数组个数的时候使用
用邻接表存储图
*/
int main()
{
vector<int> vi;
for(int i=1;i<=5;i++)
{
vi.push_back(i);
}
cout<<vi[0];//可以通过下标来访问
//通过迭代器来访问,迭代器只能用it!=vi.end()来写
for(vector<int>::iterator it=vi.begin();it!=vi.end();it++){
cout<<*it<<" "<<endl; //指针取出元素的值
}
//个数
cout<<vi.size()<<endl;
//删除尾部元素
vi.pop_back();
//插入元素
vi.insert(vi.begin()+2,-1);//在第三个位置插入-1
for(int i=0;i<vi.size();i++)
{
cout<<vi[i]<<" ";
}
//删除元素
vi.erase(vi.begin()+3);
for(int i=0;i<vi.size();i++)
{
cout<<vi[i]<<" ";
}
}
要掌握哪些内容?请看注释!!
在算法竞赛的阶段,我们只要掌握常见函数的使用方法,比如
1. 如何插入元素
2. 如何遍历元素的元素
3. 如何删除元素(增删改查已经说烂了)
4. 如何取出容器的元素个数
例题讲解
📌先来一道力扣的经典题。
移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
这里就必须提高本章开篇的那句话:数组的元素只能覆盖不能删除
暴力法
用两个for循环,一个for循环来遍历数组元素,一个for循环来更新数组内容(遇到重复的元素后面的元素都往前移动)。
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
size--; // 此时数组的大小-1
}
}
return size;
}
};
双指针法
当然本题有一种更好的解法,其实应该归结到双指针算法一类,在后面的双指针的专题中,我们还会提到它,这边写大概讲一讲针对于本题的思路,后面会详细讲解所有双指针的惯用套路和模板~
详解:
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
- 慢指针:指向更新 新数组下标的位置
用一张程序员卡尔的图(是一位算法界的大佬),画的图十分的清晰易理解。
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];
slow++;
}
}
return slow;//返回新的数组的下标
}
};
写在最后
本篇是数组一节的开章,讲解数组的基础知识和用法,下一节的主题还未定~大家可以在评论区分享建议,提出问题与想看的主题,我会酌情考虑的~😃
创作不易,喜欢的话动动小手点个免费的赞啦,谢谢大家!!~😀💜❤️