目录
一.带头双向循环链表的定义
二.带头双向循环链表的功能实现
2.1.带头双向循环链表的定义
2.2.带头双向循环链表的结点创建
2.3.带头双向循环链表的初始化
2.4.带头双向循环链表的打印
2.5.带头双向循环链表的判空
2.6.带头双向循环链表的尾插
2.7.带头双向循环链表的头插
2.8.带头双向循环链表的尾删
2.9.带头双向循环链表的头删
2.10.带头双向循环链表的在pos位置之前插入
2.11.带头双向循环链表的删除pos位置的结点
2.12.带头双向循环链表的求链表长度
2.13.带头双向循环链表的销毁
2.14.完整程序
List.h
List.c
test.c
三. 顺序表和链表的比较
逻辑结构
存储结构
基本操作
创建
销毁
增加与删除
查找
一.带头双向循环链表的定义
循环单链表虽然能够实现从任一结点出发沿着链能找到其前驱结点,但时间耗费是O(n)。如果希望从表中快速确定某一个结点的前驱,另一个解决方法就是在单链表的每个结点里再增加一个指向其前驱的指针域prior。这样形成的链表中就有两条方向不同的链,称之为双(向)链表。
与单链表类似,双链表也可增加头结点使双链表的某些运算变得方便。同时双向链表也可以有循环表,称为双向循环链表。
由于在双向链表中既有前向链又有后向链,寻找任一结点的直接前驱结点与直接后继结点都变得非常方便了。
二.带头双向循环链表的功能实现
2.1.带头双向循环链表的定义
//定义
typedef int LTDataType;typedef struct ListNode
{struct ListNode* next;struct ListNode* pre;LTDataType data;
}LTNode;
与单链表的定义不同,带头双向循环链表要定义两个指针:前驱指针pre和后继指针next。前驱指针pre用于指向当前结点的上一个结点,后继指针next用于指向当前结点的下一个结点。
2.2.带头双向循环链表的结点创建
LTNode* BuyListNode(LTDataType x)
{//动态开辟一个结点nodeLTNode* node = (LTNode*)malloc(sizeof(LTNode));//判空if (node == NULL){perror("malloc fail!");exit(-1);}//前驱与后继结点均置为空node->data = x;node->next = NULL;node->pre = NULL;return node;
}
结点的创建主要是通过调用malloc函数来实现,初始化时要将前驱指针和后继指针都置为NULL。
2.3.带头双向循环链表的初始化
版本一:
void ListInit(LTNode** phead)
{//这里需要传入二级指针,即传地址,才能实现对链表的修改//判空assert(phead);//创建头结点*phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身(*phead)->next = *phead;(*phead)->pre = *phead;
}
版本二:
LTNode* ListInit()
{//创建头结点LTNode* phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身phead->next = phead;phead->pre = phead;//返回头结点return phead;
}
链表的初始化采用了两种方式:传二级指针和设置返回值。
总结:
如果要改变头指针,就要传二级指针。不需要改变头指针的话,则传入一级指针。
在使用带头结点的单链表时:
- 初始化链表头指针需要传二级指针;
- 销毁链表需要传二级指针;
- 插入、删除、遍历、清空结点用一级指针即可。
不带头结点的单链表,除了初始化和销毁,插入、删除和清空结点也需要二级指针。
调试分析:
2.4.带头双向循环链表的打印
void ListPrint(LTNode* phead)
{//判空assert(phead);//cur指向链表的第一个结点LTNode* cur = phead->next;//cur依次向后遍历,直到cur重新回到头结点while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}
设置一个临时变量cur,指向当前链表的第一个结点(非头结点),然后依次向后遍历该链表,直到cur重新回到头结点phead的位置。
2.5.带头双向循环链表的判空
bool ListEmpty(LTNode* phead)
{//判空assert(phead);//如果phead->next等于phead,则链表为空,返回true//如果phead->next不等于phead,则链表不为空,返回falsereturn phead->next == phead;
}
如果phead->next等于phead,则链表为空,返回true;如果phead->next不等于phead,则链表不为空,返回false。
2.6.带头双向循环链表的尾插
void ListPushBack(LTNode* phead, LTDataType x)
{//判空assert(phead);//创建新结点LTNode* newnode = BuyListNode(x);//查找尾结点LTNode* tail = phead->pre;//尾插//原尾和新尾相互链接tail->next = newnode;newnode->pre = tail;//头结点和新尾相互链接newnode->next = phead;phead->pre = newnode;
}
相较于单链表的尾插,带头双向循环链表的尾插不需要从头结点开始依次向后遍历,因为头结点的前驱结点便指向尾结点tail。在找到尾结点tail之后,便可将新结点newnode插入到尾结点tail的后面。此时newnode变为新的尾结点。
调试分析:
运行结果:
2.7.带头双向循环链表的头插
void ListPushFront(LTNode* phead, LTDataType x)
{//判空assert(phead);//创建新结点LTNode* newnode = BuyListNode(x);//头插//phead newnode next:三者不分先后顺序//法一:LTNode* next = phead->next;phead->next = newnode;newnode->pre = phead;newnode->next = next;next->pre = newnode;//phead newnode phead->next:先处理后两个,再处理前两个//法二://phead->next->pre = newnode;//newnode->next = phead->next;//phead->next = newnode;//newnode->pre = phead;
}
在进行头插时,要注意结点之间插入的先后顺序,这里主要介绍两种方式。方式一:创建一个临时变量next,然后将头结点的下一个结点保存在next当中。首先调用BuyListNode(x)创建一个新结点newnode,然后将phead,newnode和next三个结点进行链接。三个结点不分先后顺序,直接进行链接即可。该方式最为简单,也最不容易出错;方式二:不创建临时变量next。首先调用BuyListNode(x)创建一个新结点newnode,然后将phead,newnode和phead->next三个结点进行链接。链接是关键:要先将后两个结点进行链接,然后再将前两个结点进行链接。三个结点一定要注意先后顺序,不可随意链接。
调试分析:
运行结果:
2.8.带头双向循环链表的尾删
void ListPopBack(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));//找尾结点LTNode* tail = phead->pre;//找尾结点的前一结点LTNode* tailPre = tail->pre;//释放尾结点free(tail);tailPre->next = phead;phead->pre = tailPre;
}
在进行尾删之前,首先要判断链表是否为空,可以通过phead->next != phead进行判断,也可以调用ListEmpty(phead)函数进行判断;然后找到链表的尾结点tail,以及链表尾结点的前一个结点tailPre;接着调用free函数释放尾结点tail,并将tailPre作为新的尾结点;最后再将新的尾结点与头结点phead进行相连即可。
调试分析:
运行结果:
2.9.带头双向循环链表的头删
void ListPopFront(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));//tail记录第一个结点之后的下一个结点LTNode* tail = phead->next->next;//释放第一个结点free(phead->next);//将头结点和tail相链接phead->next = tail;tail->pre = phead;
}
在进行头删之前,首先要判断链表是否为空,可以通过phead->next != phead进行判断,也可以调用ListEmpty(phead)函数进行判断;然后找到链表的第二个有效结点tail;接着调用free函数释放掉第一个有效结点,并将tail作为新的第一个有效结点;最后再将新的第一个结点tail与头结点phead进行相连即可。
调试分析:
运行结果:
2.10.带头双向循环链表的在pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//创建新结点LTNode* newnode = BuyListNode(x);//pre newnode pospre->next = newnode;newnode->pre = pre;newnode->next = pos;pos->pre = newnode;
}
给定一个结点pos,如果是带头双向循环链表,那么pos之前的结点和pos之后的结点都是可知的。要在pos位置之前插入,首先要找到pos的前一结点pre,然后调用BuyListNode(x)创建一个新结点newnode,接着将pre,newnode和pos三个结点进行链接即可。此时pos位置的结点将由pos变为newnode。
调试分析:
运行结果:
2.11.带头双向循环链表的删除pos位置的结点
void ListErase(LTNode* pos)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//查找pos的后一个结点LTNode* next = pos->next;//将前一个结点pre与后一个结点next相链接pre->next = next;next->pre = pre;//释放pos结点free(pos);
}
在删除pos位置的结点之前,首先要找到pos位置的前一个结点pre,然后找到pos位置的后一个结点next,接着将结点pre与next相链接,最后再调用free函数释放掉pos结点即可。
调试分析:
运行结果:
2.12.带头双向循环链表的求链表长度
int ListSize(LTNode* phead)
{//判空assert(phead);//cur指向当前链表的第一个结点LTNode* cur = phead->next;//用于记录遍历过的结点数int size = 0;//从第一个结点开始依次向后遍历,直到遍历到头结点while (cur != phead){++size;cur = cur->next;}return size;
}
调试分析:
运行结果:
2.13.带头双向循环链表的销毁
void ListDestory(LTNode* phead)
{//判空assert(phead);//cur指向当前第一个结点LTNode* cur = phead->next;while (cur != phead){//保存cur的下一个结点LTNode* next = cur->next;//删除curListErase(cur);//更新curcur = next;}//释放头结点free(phead);
}
调试分析:
运行结果:
2.14.完整程序
List.h
#pragma once#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>//带头双向循环链表//定义
typedef int LTDataType;typedef struct ListNode
{struct ListNode* next;struct ListNode* pre;LTDataType data;
}LTNode;//创建结点
LTNode* BuyListNode(LTDataType x);//初始化:版本一
//void ListInit(LTNode** phead);//初始化:版本二
LTNode* ListInit();//打印
void ListPrint(LTNode* phead);//判空
bool ListEmpty(LTNode* phead);//尾插
//不用二级指针的原因:尾插时不会改变phead,因为它带哨兵位,尾插时不会对哨兵位进行修改
void ListPushBack(LTNode* phead, LTDataType x);//头插
void ListPushFront(LTNode* phead, LTDataType x);//尾删
void ListPopBack(LTNode* phead);//头删
void ListPopFront(LTNode* phead);//在pos位置之前插入
void ListInsert(LTNode* pos, LTDataType x);//删除pos位置的结点
void ListErase(LTNode* pos);//链表长度
int ListSize(LTNode* phead);//销毁
void ListDestory(LTNode* phead);
List.c
#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"//创建结点
LTNode* BuyListNode(LTDataType x)
{//动态开辟一个结点nodeLTNode* node = (LTNode*)malloc(sizeof(LTNode));//判空if (node == NULL){perror("malloc fail!");exit(-1);}//前驱与后继结点均置为空node->data = x;node->next = NULL;node->pre = NULL;return node;
}//初始化
/*
void ListInit(LTNode** phead)
{//这里需要传入二级指针,即传地址,才能实现对链表的修改//判空assert(phead);//创建头结点*phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身(*phead)->next = *phead;(*phead)->pre = *phead;
}
*///初始化
LTNode* ListInit()
{//创建头结点LTNode* phead = BuyListNode(-1);//将头结点的前驱指针和后继指针均指向自身phead->next = phead;phead->pre = phead;//返回头结点return phead;
}//打印
void ListPrint(LTNode* phead)
{//判空assert(phead);//cur指向链表的第一个结点LTNode* cur = phead->next;//cur依次向后遍历,直到cur重新回到头结点while (cur != phead){printf("%d ", cur->data);cur = cur->next;}printf("\n");
}//判空
bool ListEmpty(LTNode* phead)
{//判空assert(phead);//如果phead->next等于phead,则链表为空,返回true//如果phead->next不等于phead,则链表不为空,返回falsereturn phead->next == phead;
}//尾插
void ListPushBack(LTNode* phead, LTDataType x)
{//判空assert(phead);/*//创建新结点LTNode* newnode = BuyListNode(x);//查找尾结点LTNode* tail = phead->pre;//尾插//原尾和新尾相互链接tail->next = newnode;newnode->pre = tail;//头结点和新尾相互链接newnode->next = phead;phead->pre = newnode;*///尾插ListInsert(phead, x);//是phead而不是phead->pre
}//头插
void ListPushFront(LTNode* phead, LTDataType x)
{//判空assert(phead);/*//创建新结点LTNode* newnode = BuyListNode(x);//头插//phead newnode next:三者不分先后顺序//法一:LTNode* next = phead->next;phead->next = newnode;newnode->pre = phead;newnode->next = next;next->pre = newnode;//phead newnode phead->next:先处理后两个,再处理前两个//法二://phead->next->pre = newnode;//newnode->next = phead->next;//phead->next = newnode;//newnode->pre = phead;*///头插ListInsert(phead->next, x);
}//尾删
void ListPopBack(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));/*//找尾结点LTNode* tail = phead->pre;//找尾结点的前一结点LTNode* tailPre = tail->pre;//释放尾结点free(tail);tailPre->next = phead;phead->pre = tailPre;*///尾删ListErase(phead->pre);
}//头删
void ListPopFront(LTNode* phead)
{//判空assert(phead);//判断链表是否为空assert(phead->next != phead);//assert(!ListEmpty(phead));/*//tail记录第一个结点之后的下一个结点LTNode* tail = phead->next->next;//释放第一个结点free(phead->next);//将头结点和tail相链接phead->next = tail;tail->pre = phead;*///头删ListErase(phead->next);
}//在pos位置之前插入x
void ListInsert(LTNode* pos, LTDataType x)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//创建新结点LTNode* newnode = BuyListNode(x);//pre newnode pospre->next = newnode;newnode->pre = pre;newnode->next = pos;pos->pre = newnode;
}//删除pos位置的结点
void ListErase(LTNode* pos)
{//判空assert(pos);//查找pos的前一个结点LTNode* pre = pos->pre;//查找pos的后一个结点LTNode* next = pos->next;//将前一个结点pre与后一个结点next相链接pre->next = next;next->pre = pre;//释放pos结点free(pos);
}//链表长度
int ListSize(LTNode* phead)
{//判空assert(phead);//cur指向当前链表的第一个结点LTNode* cur = phead->next;//用于记录遍历过的结点数int size = 0;//从第一个结点开始依次向后遍历,直到遍历到头结点while (cur != phead){++size;cur = cur->next;}return size;
}//销毁
void ListDestory(LTNode* phead)
{//判空assert(phead);//cur指向当前第一个结点LTNode* cur = phead->next;while (cur != phead){//保存cur的下一个结点LTNode* next = cur->next;//法一:删除cur//ListErase(cur);//法二:删除curfree(cur);//更新curcur = next;}//释放头结点free(phead);
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1#include"List.h"void test()
{LTNode* plist = NULL;//初始化plist = ListInit();//头插ListPushFront(plist, 1);ListPushFront(plist, 2);ListPushFront(plist, 3);ListPushFront(plist, 4);ListPushFront(plist, 5);ListPrint(plist);ListDestory(plist);ListPrint(plist);
}int main()
{test();return 0;
}
三. 顺序表和链表的比较
下面分别从逻辑结构,存储结构,基本操作的角度对顺序表和链表进行比较。
逻辑结构
都属于线性表,都是线性结构。
存储结构
基本操作
对于任何一个数据结构,基本操作基本都能归纳为创销,增删改查。其中改建立在查的基础上。
创建
销毁
增加与删除
查找
用链表还是顺序表
顺序表 | 链表 | |
弹性(可扩容) | × | √ |
增,删 | × | √ |
改 | √ | × |
表长难以预估,经常需要增加/删除元素--链表
表长可预估,查询(搜索)操作较多--顺序表