您现在的位置是:首页 >技术交流 >数据结构之单链表网站首页技术交流
数据结构之单链表
目录
对于顺序表我们发现,在此基础上仍存在了些许不足:
1.中间或头部的插入时间复杂度为O(n),有点浪费时间。
2.增容需要申请空间,拷贝数据,释放空间,会有不小的消耗。
3.增容一般是呈2倍增容的,势必会造成空间浪费。
如何解决以上问题,我们又了解到了一种新的数据结构-“链表”
1.什么是链表。
链表顾名思义,由一条链子连接表中各成员。实则链表是由每一块独立的空间组成,每一个空间里存放着数据和一个指针,每一个指针指向下一块空间的地址,依次指向,我们可以想象成为用链条串联起类,故叫链表,链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链次序实现的。
就像火车一样
我们一般将这一块空间称为一个节点,链表的末尾节点的指针是空指针。
2.链表的优点
相较于顺序表:
1.因为链表是由一块块空间构成,所以链表不存在容量不够要扩容的问题
2.根据需求申请释放空间
3.较好地解决了在头部或中间插入删除挪动数据的问题
3.单链表的实现
这里还是以存放整形数据为例:
1.单链表的结构体
如何来实现这样的结构呢?
typedef int SLdatatype;
typedef struct seqlist
{
SLdatatype data;
struct seqlist* next;
}SLnode;
可以看到这里的结构体里存放了本结构体的指针,每一个结构体里都有一个自身的指针,而这个自身的指针就代表下一个结构体的首地址,依次下去,直至到末尾next尾空。
用单链表的话来说,就是每一个节点存放着指向下一个节点地址的指针,故此我们可以创建所需的节点个数存放数据,用指针连接起来。
2.节点的创建
SLnode* SLcreatnode(SLdatatype x)
{
SLnode* newnode = (SLnode*)malloc(sizeof(SLnode));
if (newnode == NULL)
{
perror("开辟错误
");
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
每创建一个节点,就是在堆区申请一块空间用来存放数据,利用malloc开辟该结构体大小的空间,之后返回该节点,也就是这片空间。
3.参数的调用
这里强调下参数,因为我们需要改变链表,定义链表变量的时候是指针,函数的参数应设计为二级指针。
对于一个函数我们在其中定义的变量生命周期只在该作用域中,出了作用域就会被销毁,而有的函数需要去改变实参,只是简单的改变形参,是不会影响实参的,但若为实参的地址,我们可通过解引用形参来改变实参,比如:
int swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int x = 10;
int y = 20;
swap(&x, &y);
printf("%d %d", x, y);
return 0;
}
因此,这里要想通过形参改变实参,参数设计为二级指针:这里以头插为例
4.头插
void SLpushfront(SLnode** p, SLdatatype x)//传参用二级指针
{
SLnode* newnode = SLcreatnode(x);
newnode->next = *p;
*p = newnode;
}
可以看到头插是先malloc一个节点赋值给newnode,之后在将该newnode 存放到目前的头节点的next中,然后赋值给头节点,即实现头部插入。
5.尾插
void SLpushback(SLnode** p, SLdatatype x)//尾插
{
SLnode* newnode = SLcreatnode(x);
//链表为空的话
if (*p == NULL)
{
*p = newnode;
(*p)->data = x;
}
else
{
//找尾巴(前提:链表不为空)
SLnode* tail = *p;
while (tail ->next!= NULL)
{
tail = tail -> next;
}
tail->next = newnode;
(*p)->data = x;
}
}
尾插需要我们找到最后一个节点,之后先改变末尾的next,(因为保证是连接的)然后再插入。
7.头删
void SLpopfront(SLnode** p)//头删
{
assert(*p);
if ((*p) ->next== NULL)
{
free(p);
*p = NULL;
}
else
{
SLnode* top = *p;
*p = (*p) -> next;//指向下一个
free(top);
top = NULL;
}
}
头删需要分情况
8.尾删
void SLpopback1(SLnode** p)//尾删
{
SLnode* pre = NULL;
SLnode* tail = *p;
while (tail->next != NULL)
{
pre = tail;//找到尾节点前一个
tail = tail->next;
}
free(tail);//释放最后的空间
pre->next = NULL;//同时前一个的next为空
}
或者
void SLpopback2(SLnode** p)
{
SLnode* tail = *p;
while(tail->next->next)//找倒数第二个节点
{
tail = tail->next;
}
free(tail->next);//释放
tail->next = NULL;//倒数第二个next置空
}
主要找到最后一个节点或者最后一个的前一个节点(这里的两种方法),在释放掉最后一个同时,前一个节点next为NULL。可以看到第二种直接释放掉前一个的next空间,即指向最后一个结点的空间,在置空。
8.在pos节点前插入x
void SLinsert(SLnode** p, SLnode* pos, SLdatatype x)//插在pos位置前
{
SLnode* newnode = SLcreatnode(x);
if (pos == *p)
{
SLpushfront(p, x);
}
SLnode* prev=*p;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
在这里需要注意在遍历找到pos位置前的一个节点时,我们都是prev->next==pos这样去寻找,但忽略了当头节点就是pos位置时的节点,故在开始我们还需要判断是否头节点等于pos节点,如何果实就直接调用头插
9.删除pos位置节点
void SLerase(SLnode** p, SLnode* pos)//删除pos位置的节点
{
SLnode* prev = *p;
if (*p == pos)
{
SLpopfront(p);
}
else
{
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}