前面一节我们学习了解到了顺序表的相关知识,我们可以发现下面这几个问题:
1.中间/头部的插⼊删除,时间复杂度为O(N)! 这几种情况涉及到循环,结合前面复杂度讲的大O的渐进表示法,考虑最坏的情况保留高阶项可以得到时间复杂度为O(N)
结合上一节课代码
2.增容需要申请新空间,拷⻉数据,释放旧空间,运行过程中会有不小的消耗。 3.增容⼀般是呈2倍的增⻓,势必会有⼀定的空间浪费。例如,当前容量为100,满了以后增容到200,我们再继续插⼊了5个数据,后⾯没有数据插⼊了,那么就浪费了95个数据空间。
那么有没有什么其他的线性表可以解决这个问题呢?
我们希望的是
1..中间/头部的插⼊删除,时间复杂度为O(1)
2.减少或者避免增容带来的性能消耗
3.避免空间的浪费
我们一起来学习另外一种线性表——链表
链表也是线性表的一种,我们知道
线性表: 在逻辑结构[我们认为想象出来的数据组织形式]上,都是线性的,也就说是连续的⼀条直线。 在物理结构[数据在内存上的存储形式]上,不一定是线性的,通常以数组和链式结构的形式存储。
概念:链表是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的 指针链接次序实现的。
通过概念我们可以知道这里面有指针的存在,我们知道指针也就是地址。
链表也就是通过一个个的地址关系把数据元素连接起来
为了更好的理解,我们可以把链表想象成火车,火车是由一节节车厢组成的,在淡季的时候会减少火车的车厢,在旺季的时候(比如春运)会增加车厢,只需要将火车里面的某节⻋厢去掉/
加上,不会影响其他⻋厢,每节⻋厢都是独立存在的。这样就可以很好的解决车厢多人数少消耗能源大,或者人数多车厢少效率低的问题。
类比一下,我们的
链表⾥的每节"车厢"都是独立申请下来的空间,我们称之为“结点”
链表就是由一个个结点组成的
结点的组成主要有两个部分: 1.当前结点要保存的数据 2.保存下⼀个结点的地址(指针变量)
一般形式:
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;//这一个结点的数据
struct SListNode* next;//指针存放下一个结点的数据
}SLTNode;
我们用一张图来更好地理解
图中指针变量 plist保存的是第⼀个结点的地址,我们称plist此时“指向”第⼀个结点,如果我们希望
plist“指向”第⼆个结点时,只需要修改plist保存的内容为0x0012FFA0。
链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。
1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续 2、结点⼀般是从堆上申请的 (malloc申请内存空间) 3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
node1->data = 1;
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
node2->data = 2;
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
node3->data = 3;
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
node4->data = 4;
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
我们通过定义一个临时变量pcur来遍历链表,打印数据元素。
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;//pcur初始化为头结点,pcur用来遍历链表
while (pcur)//结点不为空
{
printf("%d->", pcur->data);
pcur = pcur->next;//pcur指向下一个结点
}
printf("NULL\n");
}
我们来试一试手动赋值的结果是否可以打印成功?
我们可以看到成功打印了,说明我们的代码是没有问题的。
当数据插入时,我们就需要为这个数据申请一个新的结点,我们可以写一个函数来实现这个功能。
//申请新结点
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)//没有申请成功
{
perror("malloc fail");
exit(1);//退出
}
//申请成功
node->data = x;
node->next = NULL;
return node;
}
在一个链表的尾部插入一个结点,我们首先需要申请一个新结点 有两种情况 1.如果头结点为空,也就是链表里面还没有结点,我们就把新结点赋给头结点 2.头结点不为空,就需要找尾结点(遍历链表找尾结点),将尾结点的指针变量指向新结点
这里我们首先来想一想尾插在传参的时候是传指针变量,还是传指针变量的地址呢?
答案是单链表的插入和删除都是传递的是指针变量的地址,这是因为在进行插入的时候和删除的时候会影响到第一个结点可能会发生改变——>传址(传值传参形参改变实参不改变,传址传参形参改变实参才改变)
我们可以看到如果传一级指针不能成功的插入数据,plist依然为空。
正确代码应该是传地址,也就是一个二级指针,如下:
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);//pphead存在,否则报错
//申请一个新结点
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL)
{
//如果头结点为空//也就是还没有结点
//新结点赋给头结点
*pphead = newnode;
}
else
{
//找尾结点
SLTNode* pcur = *pphead;//pcur用来遍历链表
while (pcur->next)
{
pcur = pcur->next;
}
//pcur就为尾结点
pcur->next = newnode;
}
}
我们来进行简单的测试
我们可以看到正确的打印出来了,说明我们的代码是没有问题的。
这个就比较简单,只需要将新结点的指针变量指向原来的头结点,新结点成为头结点。
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
测试:
如果只有一个结点,我们就直接进行释放就可以了,其他情况进行这个操作时,我们需要找到尾结点以及尾结点前面一个结点(将尾结点前面一个结点的next置为空)
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
//链表不能为空
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//先定义
SLTNode* ptail = *pphead;
SLTNode* prev = NULL;
//遍历链表
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
在进行头删操作之前,我们需要保存它的下一个节点作为新的头结点。
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
查找我们不需要更改链表数据,所以我们函数传参时候只需要传值就可以了。
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
assert(phead);
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
return pcur;
pcur = pcur->next;
}
return NULL;
}
插入数据就需要申请一个新的结点,如果在指定位置之前插入数据,这个时候需要改变的是指定位置前面一个结点指针变量的指向和新结点指针变量的指向。
分为两种情况:
1.如果指定位置就是头结点,这一种情况就是我们前面说的头插的情况,直接调用头插函数就可以了
2.其他情况下,我们就需要遍历链表,先找到指定位置前面一个结点,再改变结点中指针变量的指向。
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
if (pos == *pphead)
{
//头插
SLTPushFront(pphead, x);
}
else
{
SLTNode* newnode = SLTBuyNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
分为两种情况:
1.如果指定位置就是头结点,这一种情况就是我们前面说的头删的情况,直接调用头删函数就可以了
2.其他情况下,我们就需要遍历链表,先找到指定位置前面一个结点,再改变结点中指针变量的指向。
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos);
if (pos == *pphead)
{
//头删
SLTPopFront(pphead);
}
else
{
//指定位置前面的结点prev
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos之后的结点
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos);
//指定位置的下一个结点next
SLTNode* next = pos->next;
//pos->next = pos->next->next;
pos->next = next->next;
free(next);
next = NULL;
}
销毁链表我们就需要遍历链表然后进行内存空间释放。
//销毁链表
void SLTDestory(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
SList.h
#include<stdio.h>
#include<assert.h>//assert头文件
#include<stdlib.h>//malloc头文件
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;//这一个结点的数据
struct SListNode* next;//指针存放下一个结点的数据
}SLTNode;
void SLTPrint(SLTNode* phead);
SLTNode* SLTBuyNode(SLTDataType x);
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x);
void SLTErase(SLTNode** pphead, SLTNode* pos);
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos);
void SLTDestory(SLTNode** pphead);
SList.c
#include"SList.h"
//打印链表
void SLTPrint(SLTNode* phead)
{
SLTNode* pcur = phead;//pcur初始化为头结点,pcur用来遍历链表
while (pcur)//结点不为空
{
printf("%d->", pcur->data);
pcur = pcur->next;//pcur指向下一个结点
}
printf("NULL\n");
}
//申请新结点
SLTNode* SLTBuyNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)//没有申请成功
{
perror("malloc fail");
exit(1);//退出
}
//申请成功
node->data = x;
node->next = NULL;
return node;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);//pphead存在,否则报错
//申请一个新结点
SLTNode* newnode = SLTBuyNode(x);
if (*pphead == NULL)
{
//如果头结点为空//也就是还没有结点
//新结点赋给头结点
*pphead = newnode;
}
else
{
//找尾结点
SLTNode* pcur = *pphead;//pcur用来遍历链表
while (pcur->next)
{
pcur = pcur->next;
}
//pcur就为尾结点
pcur->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead && *pphead);
//链表不能为空
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//先定义
SLTNode* ptail = *pphead;
SLTNode* prev = NULL;
//遍历链表
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
assert(phead);
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
return pcur;
pcur = pcur->next;
}
return NULL;
}
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
if (pos == *pphead)
{
//头插
SLTPushFront(pphead, x);
}
else
{
SLTNode* newnode = SLTBuyNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead && pos);
SLTNode* newnode = SLTBuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos);
if (pos == *pphead)
{
//头删
SLTPopFront(pphead);
}
else
{
//指定位置前面的结点prev
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos之后的结点
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos)
{
assert(pphead && pos);
//指定位置的下一个结点next
SLTNode* next = pos->next;
//pos->next = pos->next->next;
pos->next = next->next;
free(next);
next = NULL;
}
//销毁链表
void SLTDestory(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
test.c
#include"SList.h"
void test1()
{
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
node1->data = 1;
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
node2->data = 2;
SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
node3->data = 3;
SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
node4->data = 4;
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
SLTPrint(node1);
}
void test2()
{
SLTNode* plist = NULL;
SLTPushBack(&plist,1);//指针变量也是变量,传参时传递地址才可以改变实参
//SLTPrint(plist);
SLTPushBack(&plist, 2);
//SLTPrint(plist);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPrint(plist);
/*SLTPushFront(&plist, 1);
SLTPushFront(&plist, 2);
SLTPrint(plist);
SLTPushFront(&plist, 3);
SLTPushFront(&plist, 4);
SLTPrint(plist);*/
/*SLTPopBack(&plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPopBack(&plist);
SLTPrint(plist);*/
/*SLTPopFront(&plist);
SLTPopFront(&plist);*/
/*SLTNode* ret = SLTFind(plist, 5);
if (ret == NULL)
{
printf("没有找到\n");
}
else
{
printf("找到了\n");
}*/
/*SLTNode* ret = SLTFind(plist, 3);
SLTInsert(&plist, ret, 5);
SLTPrint(plist);
SLTInsert(&plist, ret, 5);*/
/*SLTNode* ret = SLTFind(plist, 1);
SLTInsertAfter(&plist, ret, 5);
SLTPrint(plist);
SLTInsertAfter(&plist, ret, 5);*/
SLTNode* ret = SLTFind(plist, 1);
//SLTErase(&plist, ret);
SLTEraseAfter(&plist, ret);
SLTPrint(plist);
SLTDestory(&plist);
SLTPrint(plist);
}
int main()
{
//test1();
test2();
return 0;
}