顺序表的概念
线性表的定义
线性表是n个具有相同特性的数据元素的有序序列。
线性表在逻辑上可以想象成是连续的⼀条线段,线段上有很多个点
因此,线性表是⼀个⽐较简单和基础的数据结构
线性表的顺序存储-顺序表
线性表的顺序存储就是顺序表。
如果⽅格代表内存中的存储单元,那么存储顺序表中a1-a5这5个元素就是放在连续的位置上
a1 | a2 | a3 | a4 | a5 |
---|---|---|---|---|
就是⽤⼀个数组把这些元素存储起来。顺序表就是通过数组来实现的 |
顺序表的模拟实现
顺序表的实现⽅式
按照数组的申请⽅式,有以下两种实现⽅式
- 数组采⽤静态分配,此时的顺序表称为静态顺序表。
- 数组采⽤动态分配,此时的顺序表称为动态顺序表。
静态分配就是直接向内存申请⼀⼤块连续的区域,然后将需要存放的数组放在这⼀⼤块连续的区域上。
动态分配就是按需所取。按照需要存放的数据的数量,合理的申请⼤⼩合适的空间来存放数据
实现⽅式 | 优点 | 缺点 |
---|---|---|
静态分配 | 1. 不需要动态管理内存,代码书写上会⽐较⽅便。 2. 没有动态管理内存中申请以及释放空间的时间开销。 | 1. ⼀旦空间占满,新来的数据就会溢出。 2. 如果为了保险⽽申请很⼤的空间,数据量⼩的情况下,会浪费很多空间 |
动态分配 | 1. ⾃由的分配空间。数据量⼩,就⽤申请⼩内存;数据量⼤,就在原有的基础上扩 容。 | 1. 由于需要动态管理内存,代码书写上会⽐较⿇烦。 2. 动态内存的过程中会经常涉及扩容,⽽扩容需要申请空间,转移数据,释放空间。这些操作会有⼤量的时间消耗。 |
通过两者对⽐会发现,并没有⼀种实现⽅式就是绝对完美的。想要书写⽅便以及运⾏更快,就要承担空间不够或者空间浪费的情况;想要空间上合理分配,就要承担时间以及代码书写上的消耗。
在算法竞赛中,主要关⼼的其实是时间开销,空间上是基本够⽤的。因此,定义⼀个超⼤的静态数组来解决问题是完全可以接受的。因此,关于顺序表,采⽤的就是静态实现的⽅式。
创建
const int N = 1e6 + 10; // 定义静态数组的最⼤⻓度
int a[N], n; // 直接创建⼀个⼤数组来实现顺序表, n 表⽰当前有多少个元素
添加⼀个元素
尾插
//尾插
void push_back(int x)
{
a[++n] = x; // 下标为 0 的位置空出来
// 这样操作⼀般根据个⼈习惯,也可以从 0 开始计数
// 有些问题从 1 计数,处理起来可以不⽤考虑边界情况
}
// 1. 数组存满了,就不能再存了!
// 当时,我们⼀般不去管这个判断怎么写,因为我们在调⽤的时候,⾃⼰会判断合不合法,如果不合法,我们是不会调⽤的。
时间复杂度:
直接放在后⾯即可,时间复杂度为O(1)
头插
// 头插
void push_front(int x)
{
// 要把所有的元素全部右移⼀位,然后再放到头部位置
for(int i = n; i >= 1; i--)
{
a[i + 1] = a[i];
}
a[1] = x; // 把 x 放在⾸位
n++; // 不要忘记总个数 +1
}
时间复杂度:
由于需要将所有元素右移⼀位,时间复杂度为O(N)
任意位置插⼊
// 任意位置插⼊ - 在位置 p 处,插⼊⼀个 x
void insert(int p, int x)
{
for(int i = n; i >= p; i--) // 注意顺序不要颠倒
{
a[i + 1] = a[i];
}
a[p] = x;
n++; // 不要忘记总个数 +1
}
时间复杂度:
最坏情况下需要数组中所有元素右移,时间复杂度为O(N)
删除⼀个元素
尾删
void pop_back()
{
n--;
}
时间复杂度:
显然是O(1)
头删
// 头删
void pop_front()
{
// 把所有元素向前移动⼀位
for(int i = 2; i <= n; i++)
{
a[i - 1] = a[i];
}
n--; // 总个数 -1
}
时间复杂度:
需要所有元素整体左移,时间复杂度为O(N)
任意位置删除
// 任意位置删除
void erase(int p)
{
for(int i = p + 1; i <= n; i++)
{
a[i - 1] = a[i];
}
n--; // 总个数 -1
}
时间复杂度:
最坏情况下,所有元素都需要左移,时间复杂度为O(N)
查找元素
按值查找
// 查找这个数第⼀次出现的位置,找不到返回 0
int find(int x)
{
for(int i = 1; i <= n; i++)
{
if(a[i] == x)
return i;
}
return 0;
}
时间复杂度:
最坏情况下需要遍历整个数组,时间复杂度为O(N)
按位查找
// 返回 p 位置的数
int at(int p)
{
return a[p];
}
时间复杂度:
这就是顺序表随机存取的特性,只要给我⼀个下标,就能快速访问到该元素。
时间复杂度为O(1)
修改元素
// 把 p 位置的数修改成 x
void change(int p, int x)
{
a[p] = x;
}
时间复杂度:
这就是顺序表随机存取的特性,只要给我⼀个下标,就能快速访问到该元素。
时间复杂度为O(1)
清空顺序表
// 清空顺序表
void clear()
{
n = 0;
}
时间复杂度:
要注意,实现的简单形式是O(1) 。
但是,严谨的⽅式应该是O(N)
所有测试代码
#include <iostream>
using namespace std;
const int N = 1e6 + 10; // 根据实际情况⽽定
// 创建顺序表
// int a[N]; // ⽤⾜够⼤的数组来模拟顺序表
// int n; // 标记顺序表⾥⾯有多少个元素
// 需要多个顺序表,才能解决问题
int a1[N], n1;
int a2[N], n2;
int a3[N], n3;
// 打印顺序表
void print()
{
for(int i = 1; i <= n; i++)
{
cout << a[i] << " ";
}
cout << endl << endl;
}
// 尾插
void push_back(int a[], int& n, int x)
{
a[++n] = x;
}
void test()
{
push_back(a1, n1, 1);
push_back(a3, n3, 2);
}
// 头插
void push_front(int x)
{
// 1. 先把 [1, n] 的元素统⼀向后移动⼀位
for(int i = n; i >= 1; i--)
{
a[i + 1] = a[i];
}
// 2. 把 x 放在表头
a[1] = x;
n++; // 元素个数 +1
}
// 在任意位置插⼊
void insert(int p, int x)
{
// 1. 先把 [p, n] 的元素统⼀向后移动⼀位
for(int i = n; i >= p; i--)
{
a[i + 1] = a[i];
}
a[p] = x;
n++;
}
// 尾删
void pop_back()
{
n--;
}
// 头删
void pop_front()
{
// 1. 先把 [2, n] 区间内的所有元素,统⼀左移⼀位
for(int i = 2; i <= n; i++)
{
a[i - 1] = a[i];
}
n--;
}
// 任意位置删除
void erase(int p)
{
// 把 [p + 1, n] 的元素,统⼀左移⼀位
for(int i = p + 1; i <= n; i++)
{
a[i - 1] = a[i];
}
n--;
}
// 按值查找
int find(int x)
{
for(int i = 1; i <= n; i++)
{
if(a[i] == x) return i;
}
return 0;
}
// 按位查找
int at(int p)
{
return a[p];
}
// 按位修改
int change(int p, int x)
{
a[p] = x;
}
// 清空操作
void clear()
{
n = 0;
}
int main()
{
// 测试尾插
push_back(2);
print();
push_back(5);
print();
push_back(1);
print();
push_back(3);
print();
// 测试头插
push_front(10);
print();
// 测试任意位置插⼊
insert(3, 0);
print();
// 测试尾删
// cout << "尾删:" << endl;
// pop_back();
// print();
// pop_back();
// print();
// pop_front();
// pop_front();
// print();
// 测试任意位置删除
// cout << "任意位置删除:" << endl;
// erase(3);
// print();
// erase(2);
// print();
// erase(4);
// print();
for(int i = 1; i <= 10; i++)
{
cout << "查找" << i << ": ";
cout << find(i) << endl;
}
return 0;
}
封装静态顺序表
如果需要两个及以上的顺序表:
- 定义数组的时候就需要定义多个 a 1 , a 2 , … a_{1},a_{2},\dots a1,a2,… ,还需要配套的 n 1 , n 2 , … n_{1},n_{2},\dots n1,n2,…来描述顺序表的⼤⼩;
- 在调⽤push_back等函数的时候,还需要将 a 1 a_{1} a1和 n 1 n_{1} n1作为参数传进去,不然不知道修改的是哪⼀个顺序表;
- 传参的时候还需要注意传引⽤,因为顺序表的⼤⼩有可能改变,我们要修改
n
i
n_{i}
ni的值
利⽤C++中的结构体和类把我们实现的顺序表封装起来,就能简化操作
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
// 将顺序表的创建以及增删查改封装在⼀个类中
class SqList
{
int a[N];
int n;
public:
// 构造函数,初始化
SqList()
{
n = 0;
}
// 尾插
void push_back(int x)
{
a[++n] = x;
}
// 尾删
void pop_back()
{
n--;
}
// 打印
void print()
{
for(int i = 1; i <= n; i++)
{
cout << a[i] << " ";
}
cout << endl;
}
};
int main()
{
SqList s1, s2; // 创建了两个顺序表
for(int i = 1; i <= 5; i++)
{
// 直接调⽤ s1 和 s2 ⾥⾯的 push_back
s1.push_back(i);
s2.push_back(i * 2);
}
s1.print();
s2.print();
for(int i = 1; i <= 2; i++)
{
s1.pop_back();
s2.pop_back();
}
s1.print();
s2.print();
return 0;
}
⽤类和结构体将代码进⾏封装,能够很⼤程度上减少重复的操作,使代码的复⽤率⼤⼤提升
动态顺序表-vector
创建vector
#include <vector> // 头⽂件
using namespace std;
const int N = 20;
struct node
{
int a, b, c;
};
// 1. 创建
void init()
{
vector<int> a1; // 创建⼀个空的可变⻓数组
vector<int> a2(N); // 指定好了⼀个空间,⼤⼩为 N
vector<int> a3(N, 10); // 创建⼀个⼤⼩为 N 的 vector,并且⾥⾯的所有元素都是 10
vector<int> a4 = {1, 2, 3, 4, 5}; // 使⽤列表初始化,创建⼀个 vector
// <> ⾥⾯可以放任意的类型,这就是模板的作⽤,也是模板强⼤的地⽅
// 这样,vector ⾥⾯就可以放我们接触过的任意数据类型,甚⾄是 STL
vector<string> a5; // 放字符串
vector<node> a6; // 放⼀个结构体
vector<vector<int>> a7; // 甚⾄可以放⼀个⾃⼰,当成⼀个⼆维数组来使⽤。并且每⼀维都是可变的
vector<int> a8[N]; // 创建 N 个 vector
}
size/empty
- size :返回实际元素的个数;
- empty :返回顺序表是否为空,因此是⼀个 bool 类型的返回值。
- 如果为空:返回true
- 否则,返回false
时间复杂度:O(1) 。
// 2. size
void test_size()
{
// 创建⼀个⼀维数组
vector<int> a1(6, 8);
for(int i = 0; i < a1.size(); i++)
{
cout << a1[i] << " ";
}
cout << endl << endl;
// 创建⼀个⼆维数组
vector<vector<int>> a2(3, vector<int>(4, 5));
for(int i = 0; i < a2.size(); i++)
{
// 这⾥的 a2[i] 相当于⼀个 vector<int> a(4, 5)
for(int j = 0; j < a2[i].size(); j++)
{
cout << a2[i][j] << " ";
}
cout << endl;
}
cout << endl << endl;
}
begin/end
- begin :返回起始位置的迭代器(左闭);
- end :返回终点位置的下⼀个位置的迭代器(右开);
利⽤迭代器可以访问整个 vector ,存在迭代器的容器就可以使⽤范围 for 遍历
// 3. begin/end
void test_it()
{
vector<int> a(10, 1);
// 迭代器的类型是 vector<int>::iterator,但是⼀般使⽤ auto 简化
for(auto it = a.begin(); it != a.end(); it++)
{
cout << *it << " ";
}
cout << endl << endl;
// 范围 for 遍历
for(auto x : a)
{
cout << x << " ";
}
cout << endl << endl;
}
push_back/pop_back
- push_back :尾部添加⼀个元素
- pop_back :尾部删除⼀个元素
当然还有 insert 与 erase 。不过由于时间复杂度过⾼,尽量不使⽤。
时间复杂度:O(1)
// 如果不加引⽤,会拷⻉⼀份,时间开销很⼤
void print(vector<int>& a)
{
for(auto x : a)
{
cout << x << " ";
}
cout << endl;
}
// 4. 添加和删除元素
void test_io()
{
vector<int> a;
// 尾插 1 2 3 4 5
a.push_back(1);
a.push_back(2);
a.push_back(3);
a.push_back(4);
a.push_back(5);
print(a);
// 尾删 3 次
a.pop_back();
a.pop_back();
a.pop_back();
print(a);
}
front/back
- front :返回⾸元素;
- back :返回尾元素;
时间复杂度:O(1)
// 5. ⾸元素和尾元素
void test_fb()
{
vector<int> a(5);
for(int i = 0; i < 5; i++)
{
a[i] = i + 1;
}
cout << a.front() << " " << a.back() << endl;
}
resize
- 修改 vector 的⼤⼩。
- 如果⼤于原始的⼤⼩,多出来的位置会补上默认值,⼀般是 0 。
- 如果⼩于原始的⼤⼩,相当于把后⾯的元素全部删掉。
时间复杂度:O(N)
// 如果不加引⽤,会拷⻉⼀份,时间开销很⼤
void print(vector<int>& a)
{
for(auto x : a)
{
cout << x << " ";
}
cout << endl;
}
// 6. resize
void test_resize()
{
vector<int> a(5, 1);
a.resize(10); // 扩⼤
print(a);
a.resize(3); // 缩⼩
print(a);
}
clear
- 清空 vector
底层实现的时候,会遍历整个元素,⼀个⼀个删除,因此时间复杂度:O(N)
// 如果不加引⽤,会拷⻉⼀份,时间开销很⼤
void print(vector<int>& a)
{
for(auto x : a)
{
cout << x << " ";
}
cout << endl;
}
// 7. clear
void test_clear()
{
vector<int> a(5, 1);
print(a);
a.clear();
cout << a.size() << endl;
print(a);
}
所有测试代码
#include <iostream>
#include <vector>
using namespace std;
const int N = 10;
struct node
{
int a, b;
string s;
};
void print(vector<int>& a)
{
// 利⽤ size 来遍历
// for(int i = 0; i < a.size(); i++)
// {
// cout << a[i] << " ";
// }
// cout << endl;
// 利⽤迭代器来遍历 - 迭代器类型 vector<int>::iterator
// for(auto it = a.begin(); it != a.end(); it++)
// {
// cout << *it << " ";
// }
// cout << endl;
// 使⽤语法糖 - 范围 for
for(auto x : a)
{
cout << x << " ";
}
cout << endl;
}
int main()
{
// 1. 创建 vector
vector<int> a1; // 创建了⼀个名字为 a1 的空的可变⻓数组,⾥⾯都是 int 类型的数据
vector<int> a2(N); // 创建了⼀个⼤⼩为 10 的可变⻓数组,⾥⾯的值默认都是 0
vector<int> a3(N, 2); // 创建了⼀个⼤⼩为 10 的可变⻓数组,⾥⾯的值都初始化为 2
vector<int> a4 = {1, 2, 3, 4, 5}; // 初始化列表的创建⽅式
// <> ⾥⾯可以存放任意的数据类型,这就体现了模板的作⽤,也体现了模板的强⼤之处
// 这样,vector⾥⾯就可以存放我们⻅过的所有的数据类型,甚⾄是 STL 本⾝
vector<string> a5; // 存字符串
vector<node> a6; // 存结构体
vector<vector<int>> a7; // 创建了⼀个⼆维的可变⻓数组
vector<int> a8[N]; // 创建了⼀个⼤⼩为 N 的 vector 数组
// int a[N];
// 2. size / empty
// print
// print(a2);
// print(a3);
// print(a4);
// if(a2.empty()) cout << "空" << endl;
// else cout << "不空" << endl;
// 3. begin / end
// print
// print(a2);
// print(a3);
// print(a4);
// 4. 尾插以及尾删
// for(int i = 0; i < 5; i++)
// {
// a1.push_back(i);
// print(a1);
// }
// while(!a1.empty())
// {
// print(a1);
// a1.pop_back();
// }
// 5. front / back
// cout << a4.front() << " " << a4.back() << endl;
// 6. resize
vector<int> aa(5, 1);
print(aa);
// 扩⼤成 10
aa.resize(10);
print(aa);
// 缩⼩成 3
aa.resize(3);
print(aa);
// 7. clear
cout << aa.size() << endl;
aa.clear();
cout << aa.size() << endl;
return 0;
}