数据结构从零实现顺序表+链表相关操作
Posted 流浪的蚂蚁森林
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构从零实现顺序表+链表相关操作相关的知识,希望对你有一定的参考价值。
数据结构之顺序与链表
上一篇文章说到数据结构的四大基本结构,集合结构,线性结构,树结构,图结构。所以我们学习数据结构其实也是按照这个顺序进行了,从逻辑结构的层次上然后又可以去认识到-----即便是一种逻辑结构也会有不同的存储结构,顺序存储与链式存储。
OK,那我们从集合结构开始学吗?其实不必,集合其实对于我们来说并不陌生啊,C语言中的数组在某一种程度来说,我认为他就是一种集合啊,数组中存储着大量的数组或者是字符,他不就代表着数字或者字符的集合吗?如果是学过python的小伙伴们应该知道,python中直接就有一个类型就是集合。
那我们数据结构就从线性结构开始了。
常见的线性结构有那些呢?顺序表,链表,栈,队列以及字符串。我们把这些统称为线性表【后续希望可以都出一篇总结总结@_@】
本篇我们考虑顺序表和链表!
顺序表
顺序表的概念
顺序表其实就是一种特殊的数组(这里说的是动态顺序表,静态顺序表可以说就是数组了)
为什么说他是特殊的数组呢?
- 可以动态的增长
- 并且数据是连续的,必须是从左到右连续的
- 要求不能有元素与元素中间不能有空着的
什么叫动态增长?回想定义数组的时候,我们说int a[100],这个100必须是一个常量是定值(C99之前),或者不写,但是后面跟需要跟初始化就能知道数组的大小了,否则在进行操作的时候就会出现数组越界。但是现实中,我们可能也不太清楚后续究竟会使用多少空间,这就是缺陷。顺序表就完善了这个缺陷,使用malloc动态分配内存。
当然了动态增容确实完善了静态数组的缺陷,但是她本身也是有缺点了,使用malloc是有性能消耗的,同样的由于我们也不确定可能会使用多大空间,所以也许开辟的空间我们没有使用到,这也是一种浪费。
了解到了这些概念之后,我们开始创建一个顺序表瞧瞧。
struct SeqList
int* a;//使用指针,malloc创建,后续可以使用realloc扩容,这就是动态了
int size;//记录有效数据的个数
int capicity;//记录容量空间的大小
;
不知道会不会有人问前面说到顺序表是数组结果我写了个结构体出来了,这里我们所说的顺序表实际上还是操作到了a上不是吗,就像数组开辟一样,arr[100],我们不也有一个100可以表识她的大小吗,这里的capicity和size就和100的作用是一样的。就比如今天我们待了个眼睛和口罩,总不能说不是人了吧~
平时我们会因为写结构体很繁琐,创建一个顺序表要写成struct SeqList *seq,但是使用typedef就可以直接写成SeqList *seq(虽然只是少些一个单词,但是后续也不知道要写几遍,能少些一些就少些一些!)
typedef struct SeqList
SeqDataType* a;
int size;
int capacity;
SeqList;
顺序表的一些操作[函数实现]
1.初始化
void SeqListInit(SeqList *seq)
assert(seq != NULL);//终止程序了报错
seq->a = NULL;
seq->size = seq->capacity = 0;
2.打印
void SeqListPrint(SeqList* seq)
assert(seq);
for (int i = 0; i < seq->size; i++)
printf("%d ", seq->a[i]);
printf("\\n");
3.检车是否需要扩容
realloc会检测内存后面空间是否足够使用,如果够就不用增容,如果不够就会增容了,并且增容的时候还会检查增容的地方是否已经被占用了,如果被占用了还会在另一块内存开辟空间,然后将数据拷贝过去,返回新内存的地址。
所以说扩容也是有代价的。
void SeqCheckCapacity(SeqList* seq)
//满了需要增容
if (seq->size == seq->capacity)
//一般增容会多增一些2倍基准增容
int newcapacity = seq->capacity == 0 ? 4 : seq->capacity * 2;
SeqDataType* newA = realloc(seq->a, sizeof(SeqDataType) * newcapacity);
if (newA == NULL)
printf("relloc fail\\n");
exit(-1);
seq->a = newA;
seq->capacity = newcapacity;
4.尾插实现
两种实现方式,如果使用使用下面写到的任意位置删除的函数,认为是最后一个位置删除就好
void SeqListPushBack(SeqList* seq, SeqDataType x)
SeqListInsert(seq, seq->size,x);
不使用的时候直接删除size位
assert(seq);
//满了需要增容
SeqCheckCapacity(seq);
seq->a[seq->size] = x;
seq->size++;
5.头插实现
同样的两种实现方式
使用的时候
void SeqListPushBack(SeqList* seq, SeqDataType x)
SeqListInsert(seq, 0, x);
不使用的时候
void SeqListPushBack(SeqList* seq, SeqDataType x)
assert(seq);
SeqCheckCapacity(seq);//seq在这里是&s,本身就是已经是地址了,所以不要取地址了
int end = seq->size - 1;
while (end >= 0)
seq->a[end + 1] = seq->a[end];
--end;
seq->a[0] = x;
seq->size++;
6.尾删实现
使用后续封装函数
void SeqListPopBack(SeqList* seq)
SeqListErase(seq, seq->size - 1);
不使用
void SeqListPopBack(SeqList* seq)
assert(seq);
assert(seq->size>0);
--seq->size;
7.头删实现
使用后续封装函数
void SeqListPopBack(SeqList* seq)
SeqListErase(seq,0);
不使用
void SeqListPopBack(SeqList* seq)
assert(seq);
assert(seq->size>0);
int begin = 0;
while (begin < seq->size - 1)
seq->a[begin] = seq->a[begin + 1];
++begin;
seq->size--;
8.查找
int SeqListFind(SeqList* seq, SeqDataType x)
assert(seq);
for (int i = 0; i < seq->size; i++)
if (seq->a[i] == x)
return i;
return -1;
8.指定位置插入
void SeqListInsert(SeqList* seq, int pos, SeqDataType x)
assert(seq);
assert(pos >= 0&&pos<=seq->size);
SeqCheckCapacity(seq);
int end = seq->size - 1;
while (end >= pos)
seq->a[end + 1] = seq->a[end];
--end;
seq->a[pos] = x;
seq->size++;
10.指定位置删除
void SeqListErase(SeqList* seq, int pos)
assert(seq);
assert(pos >= 0 && pos < seq->size);//不能=,上面是方便尾插
int begin = pos;
for (; begin < seq->size; begin++)
seq->a[begin] = seq->a[begin + 1];
seq->size--;
11.改变指定内容
void SeqListModify(SeqList* seq, int pos, SeqDataType x)
assert(seq);
assert(pos >= 0 && pos < seq->size);
seq->a[pos] = x;
12.销毁
void SeqListDestory(SeqList* seq)
free(seq->a);
seq->a = NULL;
seq->capacity = seq->size = 0;
链表
链表的概念
说完了顺序表,我们开始学习链表了。
在此之前依旧是知道为什么会出现链表,后续可能会更好理解一些。其实在实现顺序表的过程中我们就会发现,头插的时候我们会将所有的元素后移然后空出第一个位置
其实实现的时候就发现了这个过程容易需要想清楚移动结束条件,就比尾插难了一点了,而且这对于程序来说也不好受,如果需要移动大量元素,时间复杂度会越来越大。以及上述所说的,增容也是一大消耗
所以链表就出现了,它就完美解决了这两个问题,第一它使用链式(指针结构),直接就可以在某一个位置连接上一个部分,解决了第一个问题,不用挪动大量元素。然后它开辟内存是需要就开辟,不需要就不开辟的,不仅仅结局了动态增容的消耗,甚至解决了增容可能出现的空间浪费。
怎么去理解链式结构,大家做过火车动车吧。如果说有一节火车出现了问题,那是不是可以直接把这一节车厢换下来,但是不影响其他车型连接。去百度了一番图片,甚至发现了一节车厢的火车哈哈哈哈
这里说个插曲,我上学期回家坐高铁回家的时候,车厢是14节某座,进站之后,那个高铁只要8节,吓坏我了,然后我去问附近的乘务员,人家看了我网上车票说不可能吧,这个车只有8节啊。虽说这样但是还是让我上去了,但是还是跟列车长反映了,最后大概就是后面那8节维修或者什么原因给拆了
现在是不是对于链表有了一定认识?
依旧是创建一个链表看看
通过指针达到链式的结构了
typedef struct SListNode
SLTDataType data;
struct SListNode* next;
SLTNode;
链表的结构复杂多样,还有循环链表,双向链表,带头节点链表,不带头节点链表。初学链表,我们以不带头节点不循环非双向入门链表~
链表表的一些操作[函数实现]
1.创建一个链表的节点
SLTNode* BuySLTNode(SLTDataType x)
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
node->data = x;
node->next = NULL;
return node;
2.尾插
void SListPushBack(SLTNode **pplist,SLTDataType x)
//创建新节点
SLTNode* newnode = BuySLTNode(x);
SLTNode* tail = *pplist;
//分空指针与非空指针
if (*pplist == NULL)
*pplist = newnode;
else
while (tail->next != NULL)
tail = tail->next;
//newnode是指针变量就是一个地址
tail->next = newnode;
3.头插
void SListPushFront(SLTNode** pplist, SLTDataType x)
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pplist;
*pplist = newnode;
4.尾删
void SListPopBack(SLTNode** pplist)
//没有节点的情况
if (*pplist == NULL)
return;
else if ((*pplist)->next == NULL)
free(*pplist);
*pplist = NULL;
else
SLTNode* prev = NULL;
SLTNode* tail = *pplist;
while (tail->next != NULL)
prev = tail;
tail = tail->next;
free(tail);
tail = NULL;
prev->next = NULL;//注意前一个节点的next要置空
5.头删
void SListPopFront(SLTNode** pplist)
if (*pplist != NULL)
SLTNode* tail = *pplist;
*pplist = (*pplist)->next;
free(tail);
tail = NULL;
else
return;
6.查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
SLTNode* cur = plist;
while (cur != NULL)
if (cur->data == x)
return cur;
cur = cur->next;
return NULL;
7.在pos位置之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x)
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
在单链表pos位置插入插入节点
注意这里要保存前一个节点,不然无法实现前插
void SListInsertbefore(SLTNode** pplist, SLTNode* pos, SLTDataType x)
assert(pos);
SLTNode* newnode = BuySLTNode(x);
//如果在第一个节点那插入,就是头插法
if (pos == *pplist)
newnode->next = pos;
*pplist = newnode;
else
SLTNode* prev = NULL;
SLTNode* cur = *pplist;
while (cur != pos)
prev = cur;
cur = cur->next;
prev->next = newnode;
newnode->next = pos;
8.在pos位置之后删除x
void SListEraseAfter(SLTNode* pos)
assert(pos);
if (pos->next == NULL)
return;
else
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
9.在pos位置删除节点
void SListEraseCur(SLTNode **pplist,SLTNode* pos)
assert(pos);
if (pos==*pplist)
free(*pplist);
*pplist = NULL;
else
SLTNode* cur = NULL;
SLTNode* tail = *pplist;
while (tail != pos)
cur = tail;
tail = tail->next;
cur->next = tail->next ;
free(pos);
pos = NULL;
10.打印节点
void SListPrint(SLTNode* plist)
SLTNode* cur = plist;
while (cur != NULL)
printf("%d ", cur->data);
cur = cur->next;
printf("\\n");
总结
今天就写到这里了,后续会更新其他线性结构的内容了。
要学的东西真多,但是学着有意义,希望自己可以保持乐而不疲的状态~
以上是关于数据结构从零实现顺序表+链表相关操作的主要内容,如果未能解决你的问题,请参考以下文章