最近在学数据结构和C++,数据结构是跟着比特杭哥学的,想着复习一下这段时间的学习成果。
这次介绍顺序表,C++的知识仅仅用了 cout输出和cin输入,其它基本都是C的语法,之后的文章会去介绍链表。
1.什么是线性表
简单的说,线性表就是一个数组,不过实现的时候我们把这个数组放在一个结构体里面,再记录一下有效元素的个数,就可以了。
从数据结构的角度来看,数据元素之间的逻辑关系是线性的,存储结构是顺序存储。
我们来看实现,先看看两种结构体的描述:
第一种,静态的数组
typedef int SQdateType;//把int 重定义一下,方便以后存储数据类型发生改变时去修改
typedef struct SeqList//创建一个结构体,当作顺序表来使用
{
SQdateType a[MAX];//创建一个整形数组
SQdateType size; //记录当前存放了几个数据
} SL;//给这个顺序表起一个别名,叫做SL,方便调用
第二种,动态的数组
typedef int SQdateType;//把int 重定义一下,方便以后存储数据类型发生改变时去修改
//动态版顺序表的创建
typedef struct SeqList//创建一个结构体,当作顺序表来使用
{
SQdateType* a;//定义一个指针变量来管理内存
SQdateType capacity;//记录当前顺序表的容量
SQdateType size; //记录当前存放了几个数据
} SL;//给这个顺序表起一个别名,叫做SL,方便调用
我们可以看到,第二种情况和第一个不太一样,为什么?
我们思考,直接用数组在栈区开辟空间,这个空间是死的,不能更改,如果给多了,是浪费,如果给少了无法增容,所以干脆我们直接自己开辟空间,要多少给多少。
所以就有了第二种结构体的定义方法,我们用一个指针来管理开辟的内存,size还是记录元素的个数,capacity就用来记录开辟的空间是否足够。
所以我们直接介绍第二种,它们功能的实现基本是一样的。
SQdateType就是 int 类型!!!
SL 是我们结构体的别名!!!
2.顺序表的功能
1. 初始化顺序表、打印顺序表、检查顺序表的容量 |
2.增加元素:1)头部增加 2)尾部增加 3)中间增加 |
3.删除元素: 1)头部删除 2)尾部删除 3)中间删除 |
4.查找元素:查找某个元素 |
5.修改元素:找到某个元素,把它修改掉 |
6.销毁顺序表 |
初始化顺序表:
void InitSeqList(SL *ps)
{
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
这个函数就是给我们结构体里面的元素给一个初始值。
检查顺序表的容量:
void CheckCapacity(SL *ps)
{
if (ps->size >= ps->capacity)//如果容量满了,去增容
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SQdateType *temp = (SQdateType *)realloc(ps->a, newcapacity * sizeof(SQdateType));
if (temp == NULL)
{
cout << "realloc 开辟失败" << endl;
exit(-1);
return;
}
else
{
cout << "开辟成功!!" << endl;
ps->a = temp;
ps->capacity = newcapacity;
return;
}
}
else
return;
}
检查容量的逻辑也很简单,就是在我们需要在顺序表中插入数据时,发现size和capacity是相等的,证明我们开辟的空间已经被占满了,size和capacity的单位都是“个”,就是说 几个int型空间。当满的时候,我们就用realloc函数对我们的容量进行扩容,一般是扩容到原来的两倍。扩容失败程序就结束了,扩容成功我们还让我们的a指针来指向这片连续的内存空间。
如果不熟悉malloc 和realloc的使用可以看看我之前写的一篇文章,介绍这两个函数如何用。
https://blog.csdn.net/aaa1336/article/details/125340749?spm=1001.2014.3001.5501
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
讲一下这个代码是什么意思,这是个三目运算符,判断一开始我们的容量是否为零,如果是为零,我们就直接让newcapacity为4,在再下面用realloc开辟空间,如果不为零,那么就让容量变为原来的二倍。
为啥要这样写,我们看下面的增容函数:
void CheckCapacity(SL *ps)
{
if (ps->size >= ps->capacity)//如果容量满了,去增容
{
SQdateType *temp = (SQdateType *)realloc(ps->a, 2*ps->capacity * sizeof(SQdateType));
if (temp == NULL)
{
cout << "realloc 开辟失败" << endl;
exit(-1);
return;
}
else
{
cout << "开辟成功!!" << endl;
ps->a = temp;
ps->capacity *= 2;
return;
}
}
else
return;
}
这个代码会出现一种比较尴尬的情况,因为在我们的初始化函数里面,我们一开始capacity给的是零,这个时候你调用realloc,你会申请零个字节的空间,而且我们的capacity*2还是零,所以为了避免这种情况,我们要做这样的处理。
其实一开始我们在初始化里面就为顺序表malloc一块空间也能解决这个问题。
打印顺序表
就是正常打印一个数组里面的元素,直接看代码就可以了。
void PrintSeqList(SL *ps)
{
int i = 0;
for (i = 0; i < ps->size; ++i)
{
cout << ps->a[i] << " " ;
}
cout << endl;
}
这些基本的功能介绍完成之后,我们看重要的功能。
增加元素
在顺序表中的某个位置插入元素,根据位置的不同可分为三种情况,头部插入,尾部插入,中间插入。
头插
顾名思义,就是在顺序表一个位置插入元素。
插入元素我们需要考虑:找到插入的位置,线性表的容量是否足够,如何插入。
头插,最常用的方法就是,从最后一个元素开始,依次向后移动,最后再把插入的元素放在头部的位置即可。
上代码
void PushFrontSeqList(SL *ps, SQdateType x)//在头部插入数据
//从后边往前边移动
{
CheckCapacity(ps);//先检查容量,防止移动时越界
//用循环进行移动
int end = ps->size;//end表示下表
while (end > 0)
{
ps->a[end] = ps->a[end - 1];
--end;
}
ps->a[0] = x;
ps->size++;
}
CheckCapacity用来检查容量,end用来表示下标,while循环负责移动移动元素,最后再在第一位置插入元素x,增加了一个元素,不要忘了让size增加。
这个时候若顺序表为空,则end=0,循环不会进行,直接在第一个位置插入元素。
尾插
在顺序表尾部插入元素
尾插的方法就是找到尾部,插入数据即可,我们的size记录的是有效元素的个数,但在大小上,它其实就是最后一个元素下一个位置的下标,所以我们直接在这个位置插入元素即可。
上代码:(不要忘记检查容量和让size++)
void PushBackSeqList(SL *ps, SQdateType x)//在尾部插入数据
{
CheckCapacity(ps);//检查容量
ps->a[ps->size] = x;//size代表有效元素下一个位置的下表
ps->size++;
}
任意位置插入元素
在顺序表中,任意位置插入元素,只要保证插入位置是合法的,插入即可。
在顺序表的中间插入元素(某个元素的前边),我们让从这个位置开始向后的所有元素向后边移动,再将插入的元素插入即可。
在顺序表中,任意位置插入元素,只要保证插入位置是合法的,插入即可。
在顺序表的中间插入元素(某个元素的前边),我们让从这个位置开始向后的所有元素向后边移动,再将插入的元素插入即可。
void InsertSeqList(SL *ps, int pos, SQdateType x)
//在顺序表某个 位置pos处插入元素x
//假设,这个pos是物理意义上的位置,不是数组下表,我们要将它转化成下表
//假设我们的这个插入,是把目标位置的元素统一向后移动,而不是直接覆盖
{
if (pos == 1)//这其实就是头插
{
PushFrontSeqList(ps, x);
}
else if (pos>1&&pos<ps->size)
{
pos -= 1;//转化为数组下表
int end=ps->size;//end记录下标
CheckCapacity(ps);//检查容量
while (end > pos)
{
ps->a[end] = ps->a[end - 1];
--end;
}
ps->a[pos] = x;
ps->size++;
}
else
{
cout << "插入位置不合法,无法进行数据插入" << endl;
}
}
删除元素
和插入元素类似,我们也从 头删,尾删,中间删三个方面。
头删
void PopFrontSeqList(SL *ps)//在头部删除数据
//覆盖移动
{
if (ps->size == 0)//顺序表为空,不删除。
{
cout << "顺序表为空,无法删除" << endl;
return;
}
else
{
int start = 1;
while (start < ps->size)//覆盖删除
{
ps->a[start - 1] = ps->a[start];
++start;
}
ps->size--;//最后size--
}
}
先判断顺序表是否为空,如果为空直接退出并提示,如果不为空,那么就从第二个元素开始,依次向前移动,最后size减一。
尾删
尾部删除最直接,就是让size--,不显示它就可以了。
void PopBackSeqList(SL *ps)//在尾部删除数据
//最直接的办法就是不显示它,达到删除的效果
{
if (ps->size > 0)
{
ps->size--;
}
else{
cout << "顺序表没有元素,无法删除" << endl;
}
}
任意位置删除元素
删除还是覆盖删除,从该位置后一个元素,依次向前移动,达到删除的效果。
根据输入的位置不同,也可以直接调用头删和尾删函数。
void DeleteSeqList(SL *ps, int pos)//将顺序表的某给位置pos处的元素给删除掉
{
if (pos == 1)//头删
{
PopFrontSeqList(ps);
}
else if (pos > 1 && pos < ps->size - 1)
{
//覆盖删除
int start = pos;//pos刚好是要删除位置下一个元素的下标
//所以这个pos不用再转化成下标了。
while (start < ps->size)
{
ps->a[start - 1] = ps->a[start];
++start;
}
ps->size--;
}
else//尾删
{
PopBackSeqList(ps);
}
}
查找元素
查找很简单,遍历这个数组,找到与相同的元素,返回它的下标(这里我们先不考虑重复的情况,只要第一个元素)。
int FindSeqList(SL *ps, SQdateType x)//查找某个元素,返回它的下标
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
return i;
}
return -1;
}
找到后返回下标,没找到就返回-1.
修改元素
输入要修改元素的位置,如果输入位置合法,就把它修改掉。
既然我们的查找函数可以找到下标,那么我们就把它们结合起来,利用查找函数的返回值,只要不为-1 我们就调用修改函数。
void ModifySeqList(SL *ps, SQdateType i, SQdateType x)
{
ps->a[i] = x;
return;
}
测试
我们在主函数里面测试一下我们的顺序表的功能是否正确。
int main()
{
SL plist;//创建一个顺序表
InitSeqList(&plist);//初始化顺序表
int i = 0;
for (i = 0; i < 10; i++)//用循环给顺序表赋一些值
{
CheckCapacity(&plist);//检查容量
plist.a[i] = i;//赋值
++plist.size;//有效元素个数增加
}
cout << "顺序表初始数据:" << endl;
PrintSeqList(&plist);//打印一下
cout << endl;
//结果 0 1 2 3 4 5 6 7 8 9
PushFrontSeqList(&plist, 30);//头插一个30
cout << "头插后:" << endl;
PrintSeqList(&plist);//打印一下
cout << endl;
//结果30 0 1 2 3 4 5 6 7 8 9
PushBackSeqList(&plist, 500);//尾插一个500
cout << "尾插后:" << endl;
PrintSeqList(&plist);//打印一下
cout << endl;
//结果30 0 1 2 3 4 5 6 7 8 9 500
InsertSeqList(&plist, 7, 45);//第七个位置前插入一个45
//即在5的前面插入45
cout << "第七个位置插入后:" << endl;
PrintSeqList(&plist);//打印一下
cout << endl;
//结果30 0 1 2 3 4 45 5 6 7 8 9 500
PopFrontSeqList(&plist);//头删,即把30删掉
cout << "头删后:" << endl;
PrintSeqList(&plist);//打印一下
cout << endl;
//结果 0 1 2 3 4 45 5 6 7 8 9 500
PopBackSeqList(&plist);//尾删,即把500删掉
cout << "尾删后:" << endl;
PrintSeqList(&plist);//打印一下
cout << endl;
//结果 0 1 2 3 4 45 5 6 7 8 9
DeleteSeqList(&plist, 5);//删掉第五个位置的元素,即把4删掉
cout << "第五个位置删除后:" << endl;
PrintSeqList(&plist);//打印一下
cout << endl;
//结果 0 1 2 3 45 5 6 7 8 9
int temp = FindSeqList(&plist, 7);//找到元素为7的下标
if (temp != -1)
{
ModifySeqList(&plist, temp, 80);//将这个位置的元素修改为 80
cout << "7修改为80后" << endl;
PrintSeqList(&plist);//打印一下
cout << endl;
//结果 0 1 2 3 45 5 6 80 8 9
}
else{
cout << "未找到该元素" << endl;
}
system("pause");
return 0;
}
以下是运行结果:
综上,就是我们循序表的功能实现啦。
整体来说,顺序表的功能相对简单,最重要的是理解原理!!!
分享到这里啦~~
下期再见~~~
需要源代码的家人们,可以去我的Gitee仓库直接下载哦~
链接:https://gitee.com/qiqi-loves-to-knock-code/qiqis-daily-code
文件名是顺序表哦~~