1.2 实现单链表
在上一篇文章中,单链表的实现只有一少部分,这一篇接着来了解单链表剩下的接口实现。
SList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//定义单链表就是定义节点,因为单链表是由节点组成的,所以定义单链表就是定义节点
typedef int SLTDataType;
//定义链表节点的结构
typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;//单链表的打印
void SLTPrint(SLTNode* phead);//单链表的尾插
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);//删除pos节点
void* SLTErase(SLTNode** pphead, SLTNode* pos);//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);//销毁链表
void SLTDesTroy(SLTNode** pphead);//申请新的节点
SLTNode* SLTBuyNode(SLTDataType x);
SList.c
#include"SList.h"//单链表的打印
void SLTPrint(SLTNode* phead) {SLTNode* pcur = phead;while (pcur != NULL){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("NULL\n");
}//新节点的申请
SLTNode* SLTBuyNode(SLTDataType x)
{SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));//申请新节点这里传的应该是节点类型而不是数据类型//单链表:每次插入一个节点时,需要分配一个节点(结构体)的内存,因此使用 `sizeof(节点类型)`。//顺序表:在扩容时,需要为多个数据元素分配一块连续的内存(即数组),因此使用 `元素个数 * sizeof(数据类型)`。if (node == NULL){perror("malloc fail!\n");exit(1);}node->data = x;node->next = NULL;return node;
}//单链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x) {//单链表为空assert(pphead);//SLTNode* Tail = *pphead; 不能在这里定义Tail 因为这里的Tail为空,后面循环中的Tail->next 就会报错,不会进入循环当中SLTNode* newnode = SLTBuyNode(x);if (*pphead == NULL){*pphead = newnode;}else {//单链表不为空,找尾节点,插入新节点SLTNode* Tail = *pphead;while (Tail->next != NULL){Tail = Tail->next;}//跳出循环,插入新节点Tail->next = newnode;//newnode->next = NULL; 不需要写是因为,newnode在初始化的时候就已经置为NULL了}
}//单链表的头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead &&*pphead);//pphead 不能插入空的数据//*pphead 不能对空表进行插入SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;//将新的节点作为插入后链表的头结点
}//单链表尾删
void SLTPopBack(SLTNode** pphead)
{assert(pphead&&*pphead);//pphead--空链表不可以进行删除操作// *pphead--不为空的链表--循环遍历 --遍历到尾节点的前一个节点,将此节点的next置为NULL;//只有一个节点的情况,是不能遍历的,因为他的next->next为NULLif ((*pphead)->next == NULL) // -> 的优先级大于 * 的优先级{free(*pphead);*pphead = NULL;}else {SLTNode* Tail = *pphead;SLTNode* prev = NULL;while (Tail->next){prev = Tail;Tail = Tail->next;}//跳出循环prev->next = NULL;free(Tail);Tail = NULL;}}//单链表的头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead&&*pphead);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}//单链表查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{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);//下标pos从1开始的if (pos == *pphead){//头插SLTPushFront(pphead,x);}else {SLTNode* newnode = SLTBuyNode(x);SLTNode* prev = *pphead;while (prev->next != pos){//找到prevprev=prev->next;}newnode->next = pos;prev->next = newnode;}}//在指定位置之后插入数据
void* SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x) {assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;//不用考虑pos->next为不为空的情况,因为assert只限制了pos//所以也不用单独考虑当pos->next=NULL时尾插的情况,上述的//代码包含这种情况}//删除pos节点
void* SLTErase(SLTNode** pphead, SLTNode* pos) {assert(pphead && pos);//pos为头结点if (pos == *pphead){//头删操作SLTPopFront(pphead);}//pos非头结点else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}}//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}//销毁链表
void SLTDesTroy(SLTNode** pphead) {SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
1.3 链表的分类
链表的结构多样,有8种链表结构:
链表的具体分类:
虽然链表的种类有很多,但是最常用的就两种:单链表和双链表。
单链表:单向不带头不循环的链表。结构简单,一般不会单独用来存储数据;实际中更多的是作为其他数据结构的子结构。
双链表:双向带头循环的链表 。结构最复杂,一般用在单独存储数据;实际中使用的链表数据结构,都是带头双向循环链表,虽然这个结构复杂,但是在写代码的时候会发现结构会带来很多优势,实现反而会变得更加简单。
2 双链表
2.1 概念与结构
双链表全称:带头双向循环链表
注意:
我们这里提到的 "带头" 跟我们前面说的 "头结点" 是两个概念,前面在单链表中的称呼是不严谨的,是为了更好的理解。
而在带头链表里的头结点,实际是 "哨兵位",哨兵位节点不存储任何的有效元素,只是站在这里 “放哨的”
2.2 实现双向链表
List.c
#include "List.h"LTNode* buyNode(LTDataType x)
{LTNode* node=(LTNode*)malloc(sizeof(LTNode));node->data = x;node->next = node->prev = node;return node;
}//初始化双向链表 --- 接口一致性
LTNode* LTInit()
{////创建头结点//LTNode* phead = (LTNode*)malloc(sizeof(LTNode));//phead->data = -1; //头结点是无效的数据,随便存放一数值就ok//phead->next = phead->prev = phead; //指向自己代表是双向的LTNode* phead = buyNode(-1);return phead;}//void LTInit(LTNode** pphead)
//{
// assert(pphead);
// *pphead = (LTNode*)malloc(sizeof(LTNode));
// (*pphead)->data = -1;
// (*pphead)->next = (*pphead)->prev = *pphead;
//}//销毁
void LTDesTroy(LTNode* phead)
{LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}free(phead);phead = NULL;}//打印
void LTPrint(LTNode* phead)
{LTNode* pcur = phead->next;while (pcur != phead){printf("%d ->", pcur->data);pcur = pcur->next;}printf("\n");}//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = buyNode(x);//phead phead->prev newnodenewnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;
}//头插
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);//phead newnode phead->nextLTNode* newnode = buyNode(x);phead->next->prev = newnode;newnode->next = phead->next;phead->next = newnode;newnode->prev = phead;}//判空
bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;//等于 true 为空//不等于 false 不为空}//尾删
void LTPopBack(LTNode* phead)
{assert(!LTEmpty(phead));//为空 true --- !true=false 报错//不为空 false --- !false=true 继续执行LTNode* del = phead->prev;//phead phead->prev->prev phead->prevdel->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;
}
//头删 删除的是头结点的下一个结点
void LTPopFront(LTNode* phead)
{assert(!LTEmpty(phead));//phead phead->next phead->next->nextLTNode* next = phead->next;next->next->prev = phead;phead->next = next->next;free(next);next = NULL;}//查找指定元素
LTNode* LTFind(LTNode* phead, LTDataType x)
{LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}
//在pos之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);//pos为空就是在双向链表中没有找到x,就不能进行插入操作LTNode* newnode = buyNode(x);//pos newnode pos->nextpos->next->prev = newnode;newnode->next = pos->next;pos->next = newnode;newnode->prev = pos;}
//删除在pos位置的数据
void LTErase(LTNode* pos)
{assert(pos);//pos 为空不可删除 为空找不到该元素//pos->prev pos pos->nextpos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}
3. 顺序表与链表的分析
不同点 | 顺序表 | 链表(单链表) |
存储空间 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持:O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低:O(N) | 只需要修改指针指向 |
插入 | 动态顺序表,空间不够是需要扩容和空间浪费 | 没有容量的概念,按需申请释放,不存在空间浪费 |
应用场景 | 元素高效存储+频繁访问 | 任意位置高效插入和删除 |