0.引言
我们在学习过顺序表之后,会发现两点不是很优秀的操作:
1.顺序表的头插和中间的插入:
非常麻烦,需要不断的覆盖数据。
2.动态开辟空间:
a.一般动态开辟的空间都是以2倍的形式开辟,当我们已经开辟了100个空间,并且存满了,此时我们还需要存放5个数据,那么就又需要开辟200个空间了,我们存放5个数据之后,还剩余了195个空间没有放数据,这也就导致了空间的浪费。
b. 而我们开辟新空间,拷贝数据,释放旧空间还会有一定的消耗。
注意⚠️⚠️⚠️:
我们在申请空间的时候必须要主动给它释放掉,因为申请空间的时候,是在堆上申请的,这段空间不会随着程序的结束而自然释放掉,所以要在我们程序结束之前,主动释放掉这段空间。
那有没有一种结构会很简便呢?答案肯定是有的,就是我们本次要讲解的链表。
1.链表的概念
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的 。也就是说链表的物理结构不连续,但逻辑结构连续。
我们通常定义一个结构体来表示链表的节点,结构体内部要包括节点的值和指向下一个节点的指针。
链表开始的节点称作:头节点,通常用head表示;
链表末尾的节点称作:尾结点,通常用tail表示;
其余节点称作:中间节点。
2.链表的逻辑模型
如图:在逻辑模型,可以看出它们是一个接着一个的,在逻辑上连续。
3.链表的物理模型
如图:在物理模型中,我们看到每个节点的地址都不是连续的,但是通过指针链接起来了。
4.链表的分类
4.1 单向链表
4.2 双向链表
4.3 带头单向链表(带哨兵位)
4.4带头双向链表(带哨兵位)
4.5循环单链表
4.6循环双向链表
4.7带头循环单链表
4.8带头循环双向链表
5. 单链表的实现(接口实现代码)
5.1单链表的定义
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
5.3单链表的销毁
void SLT_Destroy(SLTNode*phead)//销毁
{
while (phead != NULL)
{
SLTNode *over = phead->next;
free(phead);
phead = over;
}
}
5.4单链表的动态申请一个节点空间
SLTNode* BuySListNode(SLTDataType x) //创造新节点
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
5.5单链表的头插头删
void SLTPushFront(SLTNode** pphead, SLTDataType x) //头插
{
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SLTPopFront(SLTNode** pphead) //头删
{
// 空
assert(*pphead);
// 非空
SLTNode* newhead = (*pphead)->next;
free(*pphead);
*pphead = newhead;
}
5.6单链表的尾插尾删
void SLTPushBack(SLTNode** pphead, SLTDataType x) //尾插
{
SLTNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{
// 改变的结构体的指针,所以要用二级指针
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
// 改变的结构体,用结构体的指针即可
tail->next = newnode;
}
}
void SLTPopBack(SLTNode** pphead) //尾删
{
// 1、空
assert(*pphead);
// 2、一个节点
// 3、一个以上节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
5.7单链表的在pos位置之前的插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)//pos之前插入
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
5.8单链表的在pos位置之后的插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)//在pos之后插入
{
assert(pos);
SLTNode* newnode = BuySListNode(x);
pos->next = newnode;
newnode->next = pos->next;
}
5.9单链表的删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
//pos = NULL;
}
}
5.10单链表的删除pos位置之后的节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
// 检查pos是否是尾节点
assert(pos->next);
SLTNode* posNext = pos->next;
pos->next = posNext->next;
free(posNext);
posNext = NULL;
}
5.11单链表的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) //查找
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
6.带头双向循环链表的实现(接口实现代码)
6.1带头双向循环链表的定义
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* prev; //前驱
LTDataType data;
struct ListNode* next; //后继
}ListNode;
6.2带头双向循环链表的初始化
ListNode* ListInit() //初始化链表
{
ListNode *head = (ListNode*) malloc(sizeof (ListNode));
if(head == NULL)
{
perror("初始化开辟空间失败");
exit(-1);
}
head->data = -1;
head->prev = head;
head->next = head;
return head;
}
6.3带头双向循环链表的销毁
void ListDestory(ListNode* pHead) //销毁
{
int len = ListSize(pHead) + 1;
while (len--)
ListPopFront(pHead);
}
6.4带头双向循环链表的打印
void ListPrint(ListNode* phead) //打印
{
assert(phead);
ListNode *phead_next = phead->next;
printf("phead <-> ");
while(phead_next != phead)
{
printf("%d <-> ",phead_next->data);
phead_next = phead_next->next;
}
printf("phead\n");
}
6.5带头双向链表的增加节点
ListNode* BuyListNode(LTDataType x) //增加节点
{
ListNode *newnode = (ListNode*) malloc(sizeof (ListNode));
if(newnode == NULL)
{
perror("增加节点开辟空间失败");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
6.6带头双向循环链表的在pos之前插入
void ListInsert(ListNode* pos,LTDataType x)//在pos位置之前插入
{
assert(pos);
ListNode *newnode = BuyListNode(x);
ListNode *pos_prev = pos->prev;
pos_prev->next = newnode;
newnode->prev = pos_prev;
newnode->next = pos;
pos->prev = newnode;
}
6.7带头双向循环链表删除pos位置
void ListErase(ListNode* pos)//删除pos位置的节点
{
assert(pos);
ListNode *pos_next = pos->next;
ListNode *pos_prev = pos->prev;
pos_prev->next = pos_next;
pos_next->prev = pos_prev;
free(pos);
}
6.8带头双向循环链表的尾插尾删
void ListPushBack(ListNode* phead,LTDataType x)//尾插
{
assert(phead);
ListInsert(phead,x);
}
void ListPopBack(ListNode* phead)//尾删
{
assert(phead);
ListErase(phead->prev);
}
6.9带头双向循环链表的头插头删
void ListPushFront(ListNode* phead,LTDataType x)//头插
{
assert(phead);
ListInsert(phead->next,x);
}
void ListPopFront(ListNode* phead)//头删
{
assert(phead);
ListErase(phead->next);
}
6.10带头双向循环链表的长度求解
int ListSize(ListNode* phead)//求链表的长度
{
assert(phead);
int len = 0;
ListNode *phead_next = phead->next;
while(phead != phead_next)
{
len++;
phead_next = phead_next->next;
}
return len;
}
6.11带头双向循环链表的寻找某一节点
ListNode*ListFind(ListNode* phead, LTDataType num)//寻找某一个节点
{
assert(phead);
ListNode *find = phead->next;
while(find != phead)
{
if(find->data == num)
{
return find;
}
find = find->next;
}
return NULL;
}
7.顺序表和链表的区别
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑一定连续,物理不一定 |
任意位置插入或者删除 元素 | 需要覆盖数据 | 通过指针就能找到 |
插入 | 动态顺序表需要扩容 | 没有容量概念,需要一个给一个 |
缓存利用率 | 高 | 低 |