与你相识
![](https://img-blog.csdnimg.cn/20210530085239348.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI5NTgyNDQz,size_4,color_FFFFFF,t_70,#pic_left)
博主介绍:
– 本人是普通大学生一枚,每天钻研计算机技能,CSDN主要分享一些技术内容,因我常常去寻找资料,不经常能找到合适的,精品的,全面的内容,导致我花费了大量的时间,所以会将摸索的内容全面细致记录下来。另外,我更多关于管理,生活的思考会在简书中发布,如果你想了解我对生活有哪些反思,探索,以及对管理或为人处世经验的总结,我也欢迎你来找我。
– 目前的学习专注于Go语言,辅学算法,前端领域。也会分享一些校内课程的学习,例如数据结构,计算机组成原理等等,如果你喜欢我的风格,请关注我,我们一起成长。
Introduction
本节主要解决线性表在顺序储存这种物理结构下的具体实现,涉及到静态长度顺序表,可变长度顺序表,时间复杂度分析。
具体实现就是把数据结构这种结构填充上具体的数据,落在实处了,需要去做一些具体的操作,比如光有加减乘除,没有任何意义,但是加减乘除遇上数字就有了意义,我们我们学习的顺序表这种数据结构就仅仅是一种结构,我们需要让它实际的能够处理问题。
不变长顺序表
#include <iostream>
// 把这种常量都定义一下,这个属于编程规范方面的事情
#define LIST_INIT_SIZE 10
// 定义一个结构体,data表示数据,length表示有多少个元素
typedef struct {
int data[LIST_INIT_SIZE];
int length;
} List;
/*
* 清空一个List
*/
void ClearList(List &L) {
// 1. length置为0,现在还没有元素
L.length = 0;
// 2. 虽然L.data本身就已经开辟了一个LIST_INIT_SIZE大小的空间,但是C语言默认并不会把数组的数据设置为空,里面的数据是乱七八糟的
// 所以需要我们就遍历一下,把它的所有元素置0
for (int i = 0; i < LIST_INIT_SIZE; i++) {
L.data[i] = 0;
}
}
/*
* 构造一个空的顺序表
* char*的返回类型会有不安全的因素,所以我们加上const防止别人进行修改
*/
void InitList(List &L) {
ClearList(L);
}
/*
* 销毁一个顺序表,因为我们使用的是静态数组,在程序结束后会被程序自动回收,所以不用销毁
*/
const char *DestroList(List &L) {
return NULL;
}
/*
* 判断表是否为空
*/
bool ListEmpty(List L) {
// 如果为空返回true,如果有值返回false
return L.length == 0;
}
/*
* 返回列表的数据元素个数
*/
int ListLength(List L) {
// 返回列表的length
return L.length;
}
/*
* 用e返回L中第i个数据元素的值
*/
void GetElem(List L, int i, int &e) {
// itoa 将整型值转为字符串
if (i < 0 || i >= L.length) {
std::cout << "Get failed. Index is illegal." << std::endl;
return;
}
// 为e赋值
e = L.data[i];
}
/*
* 返回L中第1个与e相同的元素在L中的位置,若这样的数据元素不存在,返回值为-1
*/
int LocateElem(List L, int e) {
for (int i = 0; i < L.length; i++) {
// 如果找到这个元素了,返回在L中的位置
if (L.data[i] == e) {
return i;
}
}
// 如果没有找到,返回-1
return -1;
}
/*
* 若cur_e是L的数据元素,且不是第一个,则用pre_e返回其前驱,否则操作失败,pre_e无定义
*/
void PriorElem(List L, int cur_e, int &pre_e) {
// 查询一下cur_e这个元素的index
int location = LocateElem(L, cur_e);
// 如果返回值为-1,说明没找到这个元素,返回错误信息
if (location == -1) {
std::cout << "Get failed. The current element not found." << std::endl;
return;
}
// 如果返回值为0,说明这个元素是List第一个元素,没有前驱,返回错误信息。
if (location == 0) {
std::cout << "Get failed. The current element is first of the List." << std::endl;
return;
}
// 否则的情况下,就可以赋值前驱的值了
pre_e = L.data[location - 1];
}
/*
* 若cur_e是L的数据元素,且不是最后一个,则用next_e返回其后继,否则操作失败,next_e无定义
*/
void NextElem(List L, int cur_e, int &next_e) {
// 查询一下cur_e这个元素的index
int location = LocateElem(L, cur_e);
// 如果返回值为-1,说明没找到这个元素,返回错误信息
if (location == -1) {
std::cout << "Get failed. The current element not found." << std::endl;
return;
}
// 如果返回值为length - 1,说明这个元素是List最后一个元素,没有后继,返回错误信息。
if (location == L.length - 1) {
std::cout << "Get failed. The current element is last of the List." << std::endl;
return;
}
// 否则的情况下,就可以赋值后继的值了
next_e = L.data[location + 1];
}
/*
* 在L中第i个位置之前插入新的数据元素e,L的长度加1
*/
void ListInsert(List &L, int index, int e) {
// 1. 判断List是否已经满了,如果length == 最大长度的话,说明List已经满了
if (L.length == LIST_INIT_SIZE) {
std::cout << "Insert failed. The list is full." << std::endl;
return;
}
// 2. 判断索引是否合法, 首先List未满,此时索引小于0没有意义,或等于length的时候就是在末尾添加元素,超出这个索引没有意义
if (index < 0 || index > L.length) {
std::cout << "Insert failed. The index is illegal." << std::endl;
return;
}
// 3. 添加元素
// 将index ~ length的元素往后移动一位,倒着移动
for (int i = L.length; i > index; i--) {
L.data[i] = L.data[i - 1];
}
// 4. 将e赋值到index下标
L.data[index] = e;
// 5. length + 1
L.length++;
}
/*
* 删除L的第i个数据元素,L的长度减1
*/
void ListDelete(List &L, int index) {
// 1. 判断index是否合法
if (index < 0 || index >= L.length) {
std::cout << "Delete failed. The index is illegal." << std::endl;
return;
}
// 2. 删除元素
for (int i = index; i < L.length; i++) {
L.data[i] = L.data[i + 1];
}
// 3. length --
L.length--;
}
/*
* 对线性表L进行遍历,在遍历过程中对L的每个结点访问一次
*/
void TraverseList(List L) {
if (ListEmpty(L)) {
// 如果是空表,直接返回
return;
}
// 遍历List
for (int i = 0; i < L.length; i++) {
std::cout << L.data[i] << std::endl;
}
}
变长顺序表
变长顺序表实际上就是控制一下删除和添加元素的时候的策略就可以了。
在我们不变长的顺序表中,如果添加的数据以及满了,会提示错误(不能继续添加),但是在变长顺序表中,如果它满了的话,我们可以给它扩充一些长度,扩充原理:
再创建一个更长的数组,把慢的那个数组的数据移动过来,返回这个更长的数组。
倒没有什么神秘的,其实就是大瓶换小瓶。 但是光扩容可不够,在删除的时候也需要能够锁容,因为如果扩容太多,但是用不上会出现内存的冗余,那这个时候就是小瓶换大瓶。
由于我之前学习的是Java语言版的数据结构,虽然说大家根本上是相似的,但是还是会有一些差异化。
比如实现这个动态数组的方式,C语言就用了一种让我比较迷惑的方式:
C语言的数组长度必须在创建数组的时候就指定,并且是一个常数,系统会分配一个固定大小的空间,这是静态数组。
而动态数组在C语言中有malloc/free的库函数可以分配内存空间,但是分配内存空间之后需要在程序运行结束前完成释放内存的操作,如果程序运行期间分配的内存未释放,就会造成内存空间的浪费,形成所谓的内存泄漏。
而在c++中,使用new/delete关键字(基于malloc/free)也可以实现内存的扩张和释放,不过他们还是有一些细微不同,需要大家去私下了解。
既然我们是使用C语言来写的数据结构,就入乡随俗,如果需要讨论的话,也欢迎私信我,我们一起沟通。
我们本次也使用malloc/free的方式来动态分配空间。
在静态数组的基础上加了扩容和缩容的机制
#include <iostream>
#include <cstdlib>
// 把这种常量都定义一下,这个属于编程规范方面的事情
#define LIST_INIT_SIZE 2
// 定义一个结构体,data表示数据,length表示有多少个元素
typedef struct {
// 因为扩容机制的原因,我们需要使用*data基址来存储元素
int *data;
// 顺序表的元素个数
int length;
// 顺序表的最大容量
int MaxSize;
} List;
/*
* 清空一个List
*/
void ClearList(List &L) {
// 1. C语言默认并不会把开辟的内存空间的数据设置为空,里面的数据是乱七八糟的
// 所以需要我们就遍历一下,把它的所有元素置0
for (int i = 0; i < L.MaxSize; i++) {
L.data[i] = 0;
}
}
/*
* 当List满的时候,每次扩容1倍
*/
void IncreaseSize(List &L) {
// 将旧空间存储一份
int *p = L.data;
// 开辟新的存储空间,扩容一倍的长度,因为malloc返回的类型是泛型,我们需要把它转换为我们想要的类型
// 括号里的内容是字节大小,左边是我们想要多少块,右边sizeof(int)是指一块为多少字节,sizeof会算出int的字节数
L.data = (int *) malloc((L.MaxSize * 2) * sizeof(int));
// 维护最大容量
L.MaxSize = L.MaxSize * 2;
// 将新内存空间置0
ClearList(L);
// 把旧有区域的数据转移到新内存空间
for(int i = 0; i < L.length; i ++){
L.data[i] = p[i];
}
// 释放旧空间的内存
free(p);
}
/*
* 每当数据小于等于总大小的1/4的时候,缩容到1/2.
*/
void DecreaseSize(List &L) {
// 将旧空间存储一份
int *p = L.data;
// 开辟新的存储空间,砍掉一半的长度
L.data = (int *) malloc((L.MaxSize / 2) * sizeof(int));
// 维护最大容量
L.MaxSize = L.MaxSize / 2;
// 将新内存空间置0
ClearList(L);
// 把旧有区域的数据转移到新内存空间
for(int i = 0; i < L.length; i ++){
L.data[i] = p[i];
}
// 释放旧空间的内存
free(p);
}
/*
* 构造一个空的顺序表
*/
void InitList(List &L) {
// 1. 用malloc函数申请一篇连续的存储空间
L.data = (int *)malloc(LIST_INIT_SIZE * sizeof(int));
L.length = 0;
L.MaxSize = LIST_INIT_SIZE;
// 2. 对申请的内存空间区域置0
ClearList(L);
}
/*
* 销毁一个顺序表,因为我们使用的是malloc动态分配,必须需要通过free(指针)手动回收
*/
void DestroList(List &L) {
free(L.data);
L.length = 0;
L.MaxSize = 0;
}
/*
* 判断表是否为空
*/
bool ListEmpty(List L) {
// 如果为空返回true,如果有值返回false
return L.length == 0;
}
/*
* 返回列表的数据元素个数
*/
int ListLength(List L) {
// 返回列表的length
return L.length;
}
/*
* 用e返回L中第i个数据元素的值
*/
void GetElem(List L, int i, int &e) {
// itoa 将整型值转为字符串
if (i < 0 || i >= L.length) {
std::cout << "Get failed. Index is illegal." << std::endl;
return;
}
// 为e赋值
e = L.data[i];
}
/*
* 返回L中第1个与e相同的元素在L中的位置,若这样的数据元素不存在,返回值为-1
*/
int LocateElem(List L, int e) {
for (int i = 0; i < L.length; i++) {
// 如果找到这个元素了,返回在L中的位置
if (L.data[i] == e) {
return i;
}
}
// 如果没有找到,返回-1
return -1;
}
/*
* 若cur_e是L的数据元素,且不是第一个,则用pre_e返回其前驱,否则操作失败,pre_e无定义
*/
void PriorElem(List L, int cur_e, int &pre_e) {
// 查询一下cur_e这个元素的index
int location = LocateElem(L, cur_e);
// 如果返回值为-1,说明没找到这个元素,返回错误信息
if (location == -1) {
std::cout << "Get failed. The current element not found." << std::endl;
return;
}
// 如果返回值为0,说明这个元素是List第一个元素,没有前驱,返回错误信息。
if (location == 0) {
std::cout << "Get failed. The current element is first of the List." << std::endl;
return;
}
// 否则的情况下,就可以赋值前驱的值了
pre_e = L.data[location - 1];
}
/*
* 若cur_e是L的数据元素,且不是最后一个,则用next_e返回其后继,否则操作失败,next_e无定义
*/
void NextElem(List L, int cur_e, int &next_e) {
// 查询一下cur_e这个元素的index
int location = LocateElem(L, cur_e);
// 如果返回值为-1,说明没找到这个元素,返回错误信息
if (location == -1) {
std::cout << "Get failed. The current element not found." << std::endl;
return;
}
// 如果返回值为length - 1,说明这个元素是List最后一个元素,没有后继,返回错误信息。
if (location == L.length - 1) {
std::cout << "Get failed. The current element is last of the List." << std::endl;
return;
}
// 否则的情况下,就可以赋值后继的值了
next_e = L.data[location + 1];
}
/*
* 在L中第i个位置之前插入新的数据元素e,L的长度加1
*/
void ListInsert(List &L, int index, int e) {
// 1. 判断索引是否合法, 首先List未满,此时索引小于0没有意义,或等于length的时候就是在末尾添加元素,超出这个索引没有意义
if (index < 0 || index > L.length) {
std::cout << "Insert failed. The index is illegal." << std::endl;
return;
}
// 2. 判断List是否已经满了,如果length == 最大长度的话,说明List已经满了,要进行扩容
if (L.length == LIST_INIT_SIZE) {
IncreaseSize(L);
std::cout << "The list is increase size, The list Maxsize is ";
std::cout << L.MaxSize << std::endl;
}
// 3. 添加元素
// 将index ~ length的元素往后移动一位,倒着移动
for (int i = L.length; i > index; i--) {
L.data[i] = L.data[i - 1];
}
// 4. 将e赋值到index下标
L.data[index] = e;
// 5. length + 1
L.length++;
}
/*
* 删除L的第i个数据元素,L的长度减1
*/
void ListDelete(List &L, int index) {
// 1. 判断index是否合法
if (index < 0 || index >= L.length) {
std::cout << "Delete failed. The index is illegal." << std::endl;
return;
}
// 2. 删除元素
for (int i = index; i < L.length; i++) {
L.data[i] = L.data[i + 1];
}
// 3. length --
L.length--;
// 4. 判断是否需要缩容,如果需要的话,缩容1/2
if (L.length <= L.MaxSize / 4) {
DecreaseSize(L);
std::cout << "The list is decrease size, The list Maxsize is ";
std::cout << L.MaxSize << std::endl;
}
}
/*
* 对线性表L进行遍历,在遍历过程中对L的每个结点访问一次
*/
void TraverseList(List L) {
if (ListEmpty(L)) {
// 如果是空表,直接返回
return;
}
// 遍历List
for (int i = 0; i < L.length; i++) {
std::cout << L.data[i] << std::endl;
}
}
时间复杂度分析
分析顺序存储中静态数组和动态分配的时间复杂度意义不大,但是分析一下顺序表的增删查改时间复杂度,还是很有必要的。
增:O(n)
因为增加一个元素需要把这个元素的后面的所有元素后移一位。
删:O(n)
同样的,删除一个元素,也需要把这个元素后面的元素全部前移
查:O(1)
由于静态数组的内存连续的特性,我们查询一个元素的时候可以直接获取到,所以为O(1)
改:O(1)
同样的,由于内存连续,我们可以直接把某个地址的值改成我们想要的
综上,如果一个地方的查和改的需求很大,增加和删除的需求量很小,就可以使用顺序表这种数据结构了。
欢迎评论区讨论,或指出问题。 如果觉得写的不错,欢迎点赞,转发,收藏。