链表
之前我们学习了顺序表,顺序表每次进行插入和删除操作都要进行大量的数据移动,如果有成千上万的数据,那么计算机就要进行成千上万次的工作,这无疑是低效的。
那有没有一种方法,可以让删除或插入尽可能少的移动数据?
链表的原理
链表是线性表的链式存储方式,逻辑上相邻的数据在计算机内的存储位置不必须相邻,那么 怎么表示逻辑上的相邻关系呢?可以给每个元素附加一个指针域,指向下一个元素的存储位 置。如图所示:
从图中可以看出,每个结点包含两个域:数据域和指针域,指针域存储下一个结点的地址, 因此指针指向的类型也是结点类型
链表的核心要素:
每个节点由数据域和指针域组成
指针域指向下一个节点的内存地址
//链表结构体定义:
typedef int ElemType; //和#define的目的一样
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkList, LinkNode;
单链表的实现
链表的节点均单向指向下一个节点,形成一条单向访问的数据链
//单链表的初始化
typedef struct _LinkNode {
int data; //结点的数据域
struct _LinkNode *next; //结点的指针域
}LinkNode, LinkList; //链表节点、链表
bool InitList(LinkList* &L)//构造一个空的单链表 L
{
L=new LinkNode; //生成新结点作为头结点,用头指针 L 指向头结点
if(!L)return false; //生成结点失败
L->next=NULL; //头结点的指针域置空
return true;
}
//单链表增加元素
//前插法
bool ListInsert_front(LinkList* &L, LinkNode * node){
if(!L || !node ) return false;
node->next = L->next;
L->next = node;
return true;
}
//尾插法
bool ListInsert_back(LinkList* &L, LinkNode *node){
LinkNode *last = NULL;
if(!L || !node ) return false;
//找到最后一个节点
last = L;
while(last->next) last=last->next; //新的节点链接到最尾部
node->next = NULL;
last->next = node;
return true;
}
//任意位置插法
bool LinkInsert(LinkList* &L, int i, int &e)//单链表的插入
{
//在带头结点的单链表 L 中第 i 个位置插入值为 e 的新结点
int j;
LinkList *p, *s;
p=L;
j=0;
while (p&&j<i-1) //查找第 i-1 个结点,p 指向该结点
{
p=p->next;
j++;
}
if (!p || j>i-1)
{
//i>n+1 或者 i<1
return false;
}
s=new LinkNode; //生成新结点
s->data=e; //将新结点的数据域置为 e
s->next=p->next; //将新结点的指针域指向结点 ai
p->next=s; //将结点 p 的指针域指向结点 s
return true;
}
//单链表的遍历
void LinkPrint(LinkList* &L) //单链表的输出
{
LinkNode* p;
p=L->next;
while (p) {
cout <<p->data <<"\t";
p=p->next;
}
cout<<endl;
}
//单链表获取元素
bool Link_GetElem(LinkList* &L, int i, int &e)//单链表的取值
{
//在带头结点的单链表 L 中查找第 i 个元素
//用 e 记录 L 中第 i 个数据元素的值
int j;
LinkList* p;
p=L->next;//p 指向第一个结点,
j=1; //j 为计数器
while (j<i && p) //顺链域向后扫描,直到 p 指向第 i 个元素或 p 为空
{
p=p->next; //p 指向下一个结点
j++; //计数器 j 相应加 1
}
if (!p || j>i){
return false;
//i 值不合法 i>n 或 i<=0
}
e=p->data; //取第 i 个结点的数据域
return true;
}
//单链表查找元素
bool Link_FindElem(LinkList *L, int e) //按值查找
{
//在带头结点的单链表 L 中查找值为 e 的元素
LinkList *p;
p=L->next;
while (p && p->data!=e){
//顺链域向后扫描,直到 p 为空或 p 所指结点的 数据域等于 e
p=p->next; //p 指向下一个结点
}
if(!p)return false; //查找失败 p 为 NULL
return true;
}
//单链表删除元素
bool LinkDelete(LinkList* &L, int i) //单链表的删除
{ //在带头结点的单链表 L 中,删除第 i 个位置
LinkList *p, *q;
int j;
p=L;
j=0;
while((p->next)&&(j<i-1)) //查找第 i-1 个结点,p 指向该结点
{
p=p->next;
j++;
}
if (!(p->next)||(j>i-1))//当 i>n 或 i<1 时,删除位置不合理
return false;
q=p->next; //临时保存被删结点的地址以备释放空间
p->next=q->next; //改变删除结点前驱结点的指针域
delete q; //释放被删除结点的空间
return true;
}
//单链表销毁
void LinkDestroy(LinkList* &L) //单链表的销毁
{ //定义临时节点 p 指向头节点
LinkList *p = L;
cout<<"销毁链表!"<<endl;
while(p) {
L=L->next; //L 指向下一个节点
cout<<"删除元素: "<<p->data<<endl;
delete p; //删除当前节点
p=L; //p 移向下一个节点
}
}
循环链表的实现
从图中我们不难看出,循环链表只是将单链表的头结点和尾结点进行连接
双向链表的算法实现
单链表中每个结点除了存储自身数据之后,还存储了下一个结点的地址,因此可以轻松访问 下一个结点,以及后面的后继结点,但是如果想访问前面的结点就不行了,再也回不去了。 例如删除结点 p 时,要先找到它的前一个结点 q,然后才能删掉 p 结点,单向链表只能往 后走,不能向前走。如果需要向前走,我们就需要使用双向链表。
可以在单链表的基础上给每个元素附加两个指针域,一个存储前一个元素的地址,一个存储 下一个元素的地址。这种链表称为双向链表
//双链表结构体的定义
typedef struct _LinkNode {
int data; //结点的数据域
struct _LinkNode *next; //下一个节点的指针域
struct _LinkNode *prev; //上一个结点的指针域
}LinkNode, LinkList; //LinkList 为指向结构体 LNode 的指针类型
//双向链表的初始化
typedef struct _DoubleLinkNode {
int data; //结点的数据域
struct _DoubleLinkNode *next; //下一个节点的指针域
struct _DoubleLinkNode *prev; //上一个结点的指针域
}DbLinkNode, DbLinkList; //LinkList 为指向结构体 LNode 的指针类型
bool DbInit_List(DbLinkList* &L)//构造一个空的双向链表 L
{
L=new DbLinkNode; //生成新结点作为头结点,用头指针 L 指向头结点
if(!L)return false; //生成结点失败
L->next=NULL; //头结点的 next 指针域置空
L->prev=NULL; //头结点的指针域置空
L->data = -1;
return true;
}
//双向链表增加元素
//前插法
bool DbListInsert_front(DbLinkList* &L, DbLinkNode *node){
if(!L || !node) return false;
//1.只有头节点
if(L->next==NULL){
node->next=NULL;
node->prev=L; //新节点 prev 指针指向头节点
L->next=node; //头节点 next 指针指向新节点
}else { //2.还有其他结点
L->next->prev=node; //第二个节点的 prev 指向新节点
node->next = L->next; //新节点 next 指针指向第二个节点
node->prev=L; //新节点 prev 指针指向头节点
L->next=node; //头节点 next 指针指向新节点,完成插入
}
return true;
}
//尾插法
bool DbListInsert_back(DbLinkList* &L, DbLinkNode *node){
DbLinkNode *last = NULL;
if(!L || !node) return false;
last = L;
while(last->next) last = last->next;
node->next = NULL;
last->next = node;
node->prev = last;
return true;
}
//指定位置插入
bool DbLink_Insert(DbLinkList* &L, int i, int &e){
if(!L||!L->next) return false;
if(i<1) return false;
int j =0;
DbLinkList *p, *s;
p = L;
while(p && j<i){//查找位置为 i 的结点,p 指向该结点
p = p->next;
j++;
}
if(!p || j!=i){
cout<<"不存在节点:"<<i<<endl;
return false;
}
cout<<"p: "<<p<<endl;
s=new DbLinkNode;//生成新节点
s->data = e;
s->next = p;
s->prev = p->prev;
p->prev->next = s;
p->prev = s;
return true;
}
//双向链表的遍历输出
void DbLink_Print(DbLinkList* &L ){
DbLinkNode *p = NULL;
if(!L){
cout<<"链表为空."<<endl;
return ;
}
p = L;
while(p->next){
cout<<p->next->data<<"\t";
p = p->next;
}
//逆向打印
cout<<endl<<"逆向打印"<<endl;
while(p){
cout<<p->data<<"\t";
p = p->prev;
}
cout<<endl;
}
//双向链表的取值
bool DbLink_GetElem(DbLinkList* &L, int i, int &e) {
//在带头结点的双向链表 L 中查找第 i 个元素
//用 e 记录 L 中第 i 个数据元素的值
int index;
DbLinkList *p;
if(!L || !L->next) return false;
p = L->next;
index = 1;
while(p && index<i){
//顺链表向后扫描,直到 p 指向第 i 个元素或 p 为空
p = p->next; //p 指向下一个结点
index++; //计数器 index 相应加 1
}
if(!p || index>i){
return false; //i 值不合法,i>n 或 i<=0
}
e=p->data;
return true;
}
//任意位置删除
bool DbLink_Delete(DbLinkList* &L, int i) //双向链表的删除
{
DbLinkList *p;
int index = 0;
if(!L || !L->next){
cout<<"双向链表为空!"<<endl;
return false;
}
if(i<1) return false; //不能删除头节点
p=L;
while(p && index<i){
p = p->next;
index++;
}
if(!p){
//当节点不存在时,返回失败
return false;
}
p->prev->next=p->next; //改变删除结点前驱结点的 next 指针域
if(p->next){
p->next->prev = p->prev; //改变删除节点后继节点的 prev 指针域
}
delete p; //释放被删除结点的空间
return true;
}
//双向链表的销毁
void DbLink_Destroy(DbLinkList* &L) {
//定义临时节点 p 指向头节点
DbLinkList *p = L;
cout<<"销毁链表!"<<endl;
while(p){
L=L->next;//L 指向下一个节点
cout<<"删除元素: "<<p->data<<endl;
delete p; //删除当前节点
p = L; //p 移向下一个节点
}
}
链表的高阶应用
在 linux 内核中,有大量的数据结构需要用到双向链表,例如进程、文件、模块、页面等。 若采用双向链表的传统实现方式,需要为这些数据结构维护各自的链表,并且为每个链表都 要设计插入、删除等操作函数。因为用来维持链表的 next 和 prev 指针指向对应类型的对象,因此一种数据结构的链表操作函数不能用于操作其它数据结构的链表。
有没有一种方式,可以让多个链表共享同一套链表的操作?
//如下我们用两种结构体来进行链表连接
typedef struct _DoubleLinkNode { //双向链表节点“挂件”
struct _DoubleLinkNode *next; //下一个节点的指针域
struct _DoubleLinkNode *prev; //上一个结点的指针域
}DbLinkNode;
typedef struct {
int fd ;
DbLinkNode node; // 双向链表节点“挂件”
}ConnTimeout;
typedef struct {
char ed;
DbLinkNode node; // 双向链表节点“挂件”
}STAR;
//实现要点:
//使用 offsetof 可以根据链表节点在结构体中的地址逆推出结构体变量的位置
//通过节点访问到节点承载的数据
ConnTimeout *ct = new ConnTimeout;
DbLinkNode *p = &(ct->node);
cout<<"请节点对应的 fd: ";
cin>>ct->fd;
cout<<"\n 通过链表中的节点访问节点上承载的数据:"<<endl;
int offset = offsetof(ConnTimeout, node);
ConnTimeout *tmp = (ConnTimeout *)((size_t)p-offset);
printf("offset: %d\n", offset);
printf("通过链表节点 node 访问到的数据:%d\n", tmp->fd);