1. 链表分类
1.1.单链表 1.2.双链表
1.3.带头(哨兵位) 1.4.不带头
1.5.循环 1.6.不循环
一共8种链表
两种特殊链表:
2. 代码实现 带头循环双向链表
typedef LTdatatype int;//定义结构体中data的类型,方便修改
typedef struct ListNode//定义双向链表的结构体
{
struct ListNode* prev;//指向上一块空间的地址
LTdatatype data;
struct ListNode* next;//指向下一块空间的地址
}LTNode;
2.1. 创建头结点,创建节点
phead是头结点这块空间的地址,创建头结点要把prev和next都指向当前的空间
创建头结点
1.二级指针
void LTList(LTNode** phead)//创建头结点(传入二级指针,修改主函数里的空指针)
{
*phead = BuyLTNode(-1);//随便存的数字
(*phead)->prev = *phead;
(*phead)->next = *phead;
}
2.返回地址
LTNode*LTList(LTNode*phead)
{
phead=BuyLTNode(-1);
phead->prev=phead;
phead->nexr=phead;
return phead;
}
创建节点
LTNode* BuyLTNode(LTdatatype x)//返回创建节点的地址
{
LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)//判断是否创建成功
{
printf("malloc error");
exit(-1);
}
node->data=x;//创建成功输入数据
node->prev=NULL;
node->next=NULL;
return node;
}
2.2.尾插
注意:next和prev指向的是前一块结构体的地址,不是直接前一个prev与下一个的next相连
相比于单链表尾插,带头双向链表不需要判断链表是否为空,在传入的时候已经将链表的head传入,只需要在后面插入即可。
只传入头时,尾插也可以使用
void LTPushBack(LTNode*phead,LTdatatype x)
{
assert(phead);//判断传入的是不是空指针(哪怕链表里没东西,哨兵位也要有指针)
LTNode*tail=phead->prev;//找到最后一块空间的地址
LTNode* newnode=BuyLTNode(x);//创建新的空间
newnode->prev=tail;//连接最后一块空间和newnode
tail->next=newnode;
newnode->next=phead;//完成头和尾的连接,实现循环
phead->prev=newnode;
2.3.输出
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;//phead后面的节点
printf("phead<->");
while (cur != phead)//往后遍历,直到遇见phead停下
{
printf("%d<->", cur->data);
cur = cur->next;
}
prinft("phead\n");
}
2.4.尾删
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);//判断链表是否为空
LTNode* tail = phead->prev->prev;
LTNode* del = phead->prev;
free(del);
tail->next = phead;
phead->prev = tail;
}
链表空了就会报警告
2.5.头插
1.创建新的变量存储phead->next(方便理解)
void LTPushFront(LTNode* phead, LTdatatype x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x);
LTNode* cur = phead->next;
newnode->next = cur;
newnode->prev = phead;
phead->next = newnode;
cur->prev = newnode;
}
2.不创建新的参数(搞清楚先后指针指向的顺序,顺序搞混就寄)
void LTPushFront(LTNode* phead, LTdatatype x)
{
assert(phead);
LTNode* newnode = BuyLTNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
2.6.头删
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* cur = phead->next->next;
LTNode* del = phead->next;
free(del);
phead->next = cur;
cur->prev = phead;
}
2.7.数据Size
类似于LTPrint,将链表包含数据的空间全部遍历一遍
int LTSize(LTNode* phead)
{
assert(phead);
int size = 0;
LTNode* cur = phead->next;
while (cur != phead)
{
size++;
cur = cur->next;
}
return size;
}
如果结构体的data只是int类型,我们可以将head中的data作为个数,当数据删除或者增加的时候直接对phead->data修改即可
但是这里我们的数据类型是用LTdatatype定义的,就不能使用上述方法存数据数量(如果LTdatatype是char类型,int就不能正常使用)
2.7.查找
直接遍历查找即可
LTNode* LTFind(LTNode* phead, LTdatatype x)
{
assert(phead);
LTNode* cur = phead->next;
while (cur->data != x&&cur!=phead)
{
cur = cur->next;
}
return cur;
}
2.8.pos前插入
void LTInsert(LTNode* pos, LTdatatype x)
{
assert(pos);
LTNode* cur = pos->prev;
LTNode* newnode = BuyLTNode(x);
newnode->next=pos;
pos->prev = newnode;
cur->next = newnode;
newnode->prev = cur;
}
与上面头插类似,只需要找到pos前一块空间的位置,然后将新的空间newnode与前后空间连接起来即可
2.9.删除pos位
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* posprev = pos->prev;
LTNode* posnext = pos->next;
free(pos);
posprev->next=posnext;
posnext->prev = posprev;
}
3.链表和顺序表的区别
不同点 | 顺序表 | 链表 |
储存空间上 | 物理上一定连续 | 逻辑上连续,物理上不一定连续 |
随机访问 | 支持O(1)(优) | 不支持O(N)(缺) |
任意位置插入或者删除元素 | 可能需要移动元素,效率低O(N)(缺) | 只需要修改指针指向(优) |
插入 | 动态顺序表,空间不够时需要扩容(缺) | 没有容量的概念(优) |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率 | 高 | 低 |
在循环输出等情况下,顺讯表效率更快
与内存向寄存器输入数据相关,内存输入数据,会将输入数据的左右两侧地址的数据也传入寄存器(每次传入数据的量是固定的,不管传入多少数据,代价是相同的,而且车坐满了再发车划算),因为顺序表是个数组,左右两侧的数据就是需要的数据,效率会更高,
链表的每块空间可能不连续,所以传入的也只是需要的那一块空间