双向链表----“双轨联动,高效运行” (第九讲)
一. 链表的分类
链表的结构非常多样,按照以下的组合起来就有8种:
(1)带头或者不带头:
带头链表中的“头结点”,不存储任何有效的数据,只是用来占位的,我们称之为“哨兵位”。
在前面的文章中,有时候表述“头结点”,但实际上单链表中把第一个节点称为“头结点”这种说法是错误的。
(2)单向或者双向:
(3)循环或者不循环
虽然这么多的链表结构,但是我们最常用的有两种:单链表(不带头单向不循环链表)和双链表(带头双向循环链表)。
二. 双向链表
2.1概念与结构
双向链表由一个一个的节点组成,这里的节点包括3个部分。
struct ListNode{int data;struct ListNode* prev;struct ListNode* next;
};
注:当双向链表为空时,这里表示双链表中只有一个哨兵位,且它的前驱指针和后继指针都指向自身。图示如下:
2.2 双向链表的初始化
#pragma once
#include <stdio.h>
#include <stdlib.h>
typedef int LTDataType;
typedef struct ListNode {LTDataType data;LTNode* prev;LTNode* next;
}LTNode;//1.初始化
void LTInit(LTNode** phead);
#include "List.h"
void LTInit(LTNode** pphead)
{*pphead = (LTNode*)malloc(sizeof(LTNode));if (*pphead == NULL){perror("malloc fail!");exit(1);}(*pphead)->data = -1;//哨兵位节点不存储任何有效数据,-1为无效(*pphead)->next = (*pphead)->prev = NULL;
}
现在,我们已经有了一个空的双向链表,接下来,我们就要对其“增删改查”了。注意:在双向链表中,增删改查都不会改变哨兵位节点。
2.3 尾插
LTNode* LTBuyNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));while (newnode == NULL){perror("malloc fail!");exit(1);}newnode->prev = newnode->next = newnode;newnode->data = x;return newnode;
}
//2.尾插
LTNode* pushback(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;
}
注:在尾插时,我们要特别注意几个指针的顺序,应该先处理newnode的前驱指针和后继指针,再处理原来双链表中尾节点的后继指针,使其指向newnode,最后处理头节点的前驱指针,使其指向最后一个节点(此时也就是newnode)。(此处一定要注意顺序,如果错乱,就有可能找不到原来的头节点)。
2.4 头插
头插时,节点是插在哨兵位的前面,还是插在哨兵位和下一个节点的中间?显然,答案是后者。因为哨兵位不存储有效数据,并不可以算作是双链表的第一个节点。
//3.头插
LTNode* pushfront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}
2.5 尾删
//4.判断链表是否为空
bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}
//5.尾删
void popback(LTNode* phead)
{if (!LTEmpty(phead)){LTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;}
}
2.6 打印双链表
//6.打印双链表
void LTPrint(LTNode* phead)
{LTNode* pcur = phead;while (pcur != phead){printf("%d-> ", pcur->data);pcur = pcur->next;}printf("\n");
}
2.7 头删
//7.头删
LTNode* popfront(LTNode* phead)
{assert(!LTEmpty(phead));LTNode* del = phead->next;del->next->prev = phead;phead->next = del->next;free(del);del = NULL;
}
2.8 查找
//8.查找
LTNode* find(LTNode* phead, LTDataType x)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}//未找到return NULL;
}
2.9 在指定位置之后插入数据
//9.在pos位置之后插入数据
void LTInit(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));newnode->prev = pos;newnode->next = pos->next;pos->next->prev = newnode;pos->next = newnode;
}
2.10 删除指定位置的节点
//10.删除pos位置的节点
void erase(LTNode* pos)
{assert(pos);pos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}
2.11 销毁双链表
//11.销毁双链表
void LTDestory(LTNode** pphead)
{LTNode* pcur = (*pphead)->next;while (pcur != *pphead){LTNode* next = pcur->next;free(pcur);pcur = next;}//销毁头节点free(*pphead);*pphead = NULL;
}
三. 代码改进
由上述的代码可以看出,除了初始化和释放形参是二级指针,其余功能实现形参都是一级指针,那我们为了保持接口一致性,能不能初始化和释放这两个功能也弄成一级指针呢?具体方法如下:
3.1 初始化
//1.初始化
LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);return phead;
}
3.2 销毁
void destory(LTNode* phead)
{LTNode* pcur = (phead)->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}//销毁头节点free(phead);phead = NULL;
}
但是,这种写法需要自己手动将实参置为空。
四. 顺序表与链表分析
不同点 | 顺序表 | 链表(单链表) |
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持O(1) | 不支持: |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容和空间浪费 | 没有容量的概念,按需申请释放,不存在空间浪费 |
应用场景 | 元素高效存储+频繁访问 | 任意位置高效插入和删除 |
以上就是今天的内容,到目前为止,顺序表和链表就告一段落啦~喜欢的朋友们可以一键三连哦~