【数据结构入门】顺序表(SeqList)详解(初始化、增、删、查、改)
为什么有了顺序表,还需要有链表这样的数据结构呢?
- 顺序表在中间/头部的插入删除,要挪动很多数据,时间复杂度为O(N),效率太低了。
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是一次增长2倍,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们
再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
前面学习的顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,而链表是一种物理存储结构上不连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,可以实现更加灵活的动态内存管理。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
每个结点包括两个部分:
1、数据域:存储数据元素
2、指针域:存储下一个结点地址
可以看到,4个节点的地址并不是连续的,链表在物理结构上不一定是线性的,而在逻辑结构上是线性的
1、链式结构在逻辑上是连续的,但在物理上不一定连续
2、链表的节点是在堆上申请出来的
链表的结构非常多样化
首先新建一个工程( 博主使用的是 VS2019 )
如图:
typedef int SLTDataType;
//定义单链表节点
typedef struct SListNode
{
SLTDataType data; //数据域
struct SListNode* next; //指针域
}SListNode;
//动态申请一个节点
SListNode* BuySListNode(SLTDataType x)
{
SListNode* node = (SListNode*)malloc(sizeof(SListNode));
if (node == NULL) //检查是否开辟成功
{
perror("malloc");
return;
}
node->data = x;
node->next = NULL;
return node;
}
//销毁单链表中所有节点
void SListDestory(SListNode** pphead)
{
assert(pphead);
SListNode* cur = *pphead;
while (cur != NULL) //遍历链表
{
SListNode* next = cur->next; //保存cur的下一个节点
free(cur); //释放节点
cur = next;
}
*pphead = NULL; //z
}
销毁单链表中所有节点,指向头节点的指针置空
![]()
//打印单链表
void SListPrint(SListNode* phead)
{
SListNode* ptr = phead;
while (ptr != NULL)
{
printf("%d->", ptr->data);
ptr = ptr->next;
}
printf("NULL\n");
}
传一级指针的值,用一级指针接收
![]()
因为当链表为空时,我们需要改变 plist 的指向,使其指向第一个节点。
而初始 plist 和 phead 都指向 NULL,调用函数后,phead 指向了新的节点,而 plist 还是指向 NULL 的。
![]()
plist 是指向第一个节点的指针,想要在函数中改变 plist 的值(指向),必须要把 plist指针的地址 作为实参传过去,形参用 二级指针 接收,这样在函数中对二级指针解引用得到 plist 的值,就可以改变 plist 的值(指向)了
记住:在函数里面要改变 int,则要传 int* ,要改变 int* ,则要传 int**
平时一般不用解引用两层,没啥意义,一般是解引用一层,来改变外面 plist 的指向。
![]()
单链表为空时,plist 直接指向新节点;
单链表不为空时,先找到单链表的尾节点,然后将尾节点的next指向新节点
//单链表尾插
void SListPushBack(SListNode** pphead, SLTDataType x)
{
assert(pphead); //检查参数是否传错
SListNode* newnode = BuySListNode(x); //动态申请一个节点
if (*pphead == NULL) //单链表中没有节点时
{
*pphead = newnode; //plist指向新节点
}
else if (*pphead != NULL) //单链表中已经有节点时
{
SListNode* tail = *pphead;
while (tail->next != NULL) //找到单链表中的最后一个节点
{
tail = tail->next;
}
tail->next = newnode; //最后一个节点的next指向新节点
}
}
因为要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址
//单链表头插
void SListPushFront(SListNode** pphead, SLTDataType x)
{
assert(pphead); //检查参数是否传错
SListNode* newnode = BuySListNode(x); //动态申请一个节点
newnode->next = *pphead; //新节点的next指针指向plist指向的位置
*pphead = newnode; //plist指向头插的新节点
}
单链表只有一个节点时,删除节点,plist 指向 NULL;
单链表有多个节点时,先找到单链表尾节点的上一个节点,删除尾节点,然后将该节点的next指向 NULL;
因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。
//单链表尾删
void SListPopBack(SListNode** pphead)
{
assert(pphead); //检查参数是否传错
assert(*pphead); //断言,链表不能为空
SListNode* tail = *pphead;
if ((*pphead)->next == NULL) //链表只有一个节点
{
free(*pphead); //删除节点
*pphead = NULL; //plist置空
}
else //链表中有多个节点
{
while (tail->next->next != NULL) //找到链表的尾节点的上一个节点
{
tail = tail->next;
}
free(tail->next); //删除尾节点
tail->next = NULL; //置空
/*思路2:
SListNode* prev = *pphead;
while (tail->next) //找到链表的尾节点和它的上一个节点
{
prev = tail;
tail = tail->next;
}
free(tail); //删除尾节点
prev->next = NULL; //置空
*/
}
}
因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。
//单链表头删
void SListPopFront(SListNode** pphead)
{
assert(pphead); //检查参数是否传错
assert(*pphead); //链表不能为空
SListNode* cur = *pphead; //保存头节点的地址
*pphead = cur->next; //plist指向头节点的下一个节点
free(cur); //删除头节点
}
如果查找到,返回该节点的地址;没有查找到,返回 NULL。
//在单链表中查找指定值节点
SListNode* SListFind(SListNode* phead, SLTDataType x)
{
SListNode* cur = phead;
//遍历链表
while (cur != NULL)
{
if (cur->data == x)
{
return cur; //找到了,返回该节点的地址
}
cur = cur->next;
}
return NULL; //未找到,返回NULL
}
单链表不适合在pos位置之前插入,因为需要遍历链表找到pos位置的前一个节点,
单链表更适合在pos位置之后插入,如果在后面插入,只需要知道pos位置就行,会简单很多
C++官方库里面单链表给的也是在之后插入
//单链表在指定pos位置之后插入
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
assert(pos); //给的pos位置不能为空
SListNode* newnode = BuySListNode(x); //动态申请一个节点
newnode->next = pos->next; //新节点的next指针指向pos位置后一个节点
pos->next = newnode; //pos位置的next指向新节点
}
先调用 SListFind 函数在单链表中查找指定值的节点,查找到了,返回该节点的地址,也即是我们指定的 pos 位置,然后将其传给 SListInsertAfter 函数。
![]()
要考虑到两种情况:pos位置为单链表的第一个节点,pos位置为中间节点;
因为可能要改变外部 plist 的指向,所以用二级指针接收指针 plist 的地址。
//单链表删除指定pos位置的节点
void SListErase(SListNode** pphead, SListNode* pos)
{
assert(pphead);
assert(*pphead); //链表不能为空
assert(pos); //给的pos位置不能为空
//pos位置为第一个节点,相当于头删
if (pos == *pphead)
{
SListPopFront(pphead);
}
//pos位置为中间节点
else
{
SListNode* prev = *pphead;
while (prev->next != pos) //找到pos位置的前一个节点
{
prev = prev->next;
}
prev->next = pos->next; //pos位置的前一个节点指向pos位置的后一个节点
free(pos); //释放pos节点
pos = NULL; //置空
}
}
assert 放在函数里面检查参数,一方面是为了安全,另一方面也是为了防止有人调用该函数时,不正确的使用,错误传入参数,好及时提醒到他,写代码时一定要考虑到有人不正确的使用该函数时的场景,来进行避免
先调用 SListFind 函数在单链表中查找指定值的节点,查找到了,返回该节点的地址,也即是我们指定的 pos 位置,然后将其传给 SListErase 函数。
![]()
//单链表删除指定pos位置之后的节点
void SListEraseAfter(SListNode* pos)
{
assert(pos); //给的pos位置不能为空
assert(pos->next); //给的pos位置不能是尾节点
SListNode* posnext = pos->next; //保存pos位置的后一个节点
pos->next = pos->next->next;
free(posnext); //释放pos位置的后一个节点
}
//求单链表长度
int SListSize(SListNode* phead)
{
int size = 0;
SListNode* cur = phead;
while (cur != NULL) //遍历链表
{
size++;
cur = cur->next;
}
return size;
}
plist为空,返回 1 (true),非空,返回 0 (false)
//单链表判空
bool SListEmpty(SListNode* phead)
{
//plist为空,返回1(true),非空,返回0(false)
return phead == NULL;
/*写法2:
return phead == NULL ? true : false;
*/
}
大家快去动手去实现一下吧!