双链表
单链表与双链表对比
- 单链表只有一个指向后继结点的指针,所以如果给定结点p想要找到他的前趋结点很麻烦,无法逆向检索
- 双链表在单链表的基础上,增加了一个指针域,指向其前趋结点。可进可退,存储密度更低
typedef struct DNode{ //定义双链表结点数据类型
ElemType data; //数据域
struct DNode *prior,*next; //前趋和后继指针
}DNode,*DLinkList;
带头节点双链表的初始化
typedef struct DNode{ //定义双链表结点数据类型
ElemType data; //数据域
struct DNode *prior,*next; //前趋和后继指针
}DNode,*DLinkList;
bool InitDLinkList(DLinkList &L){
L=(DNode *)malloc(sizeof(DNode)); //分配一个头节点
if(L==NULL)
return false;
L->prior=NULL; //头节点永远指向NULL
L->next=NULL;
return true;
}
void testDLinkList(){
//初始化双链表
DLinkList L;
InitDLinkList(L);
//后续代码
}
}
//判断双链表是否为空
bool Empty(DLinkList L){
if(L->next==NULL)
return true;
else
return false;
}
双链表的插入
1.在p结点之后插入s结点
bool InsertNextDNode(DNode *p,DNode *s){
s->next=p->next;
p->next->prior=s; //p的后继结点的前向指针指向s
s->prior=p;
p->next=s;
}
如果p是最后一个结点,则p-next-prior=s;
这一句会出现问题
bool InsertNextDNode(DNode *p,DNode *s){
if(p==NULL}||s==NULL)
return false;
s->next=p->next;
if(p->next!=NULL) //如果p有后继结点
p->next->prior=s; //p的后继结点的前向指针指向s
s->prior=p; //跳过,就相当于,不需要调整p的后继结点的前向指针
p->next=s;
return true;
}
注意代码的顺序
**前插操作:**找到某结点的前趋,对他进行后插
双链表的删除
删除p的后继结点q
p->next=q->next;
q-next-prior=p;
free(q);
如果q结点是最后一个结点
bool DeleteNextDNode(DNode *p){
if(p==NULL) return false;
DNode *q=p->next; //找到p的后继结点q
if(q==NULL) return false; //p没有后继
p->next=q->next;
if(q->next!=NULL) //q结点不是最后一个结点
q->next->prior=p;
free(q); //释放结点空间
return true;
}
双链表的销毁
void DestoryList(DLinklist &L){
//循环释放各个数据结点
while(L->next!=NULL)
DeleteNextDNode(L);
free(L); //释放头节点
L=NULL; //头指针指向NULL
}
双链表的遍历
//后向遍历
while(p!=NULL){
p=p->next;
//对结点p的相应处理
}
//前向遍历
while(p!=NULL){
p=p->prior;
//对结点p做相应的处理
}
//前向遍历(跳过头结点)
while(p->prior!=NULL){
p=p->prior;
//对结点p做相应的处理
}
双链表不可随机存取,按位查找,按值查找的操作都只能使用遍历的方式实现,时间复杂度为O(n)
循环链表
- 单链表:表尾结点next指针指向NULL
- 循环单链表:表尾结点的next指针指向头结点
typedef struct LNode{
ElemType data; //定义单链表结点类型
struct LNode *next; //每个结点存放一个数据元素
}LNode,*LinkList; //指针指向下一个结点
//初始化一个循环链表
bool InitList(LinkList &L){
L=(LNode*)malloc(sizeof(LNode)); //分配一个头结点
if(L==NULL)
return false; //由于内存不够,分配失败
L->next=L; //头结点next指针指向头指针
return true;
}
//判断循环单链表是否为空
bool Empty(LinkList L){
if(L->next==L)
return true;
else
return false;
}
//判断扫描指针p是否是表尾结点
bool isTail(LinkList L,LNode *p){
if(p->next==L)
return true;
else
return false;
}
- 从单链表的一个结点出发只能找到后续的各个结点
- 从一个结点出发可以找到其他任何一个结点
循环双链表
- 双链表:表头结点的prior指向NULL,表尾结点的next指向NULL
- **循环双链表:**表头结点的prior指向表尾结点,表尾结点的next指向头结点
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*LinkList;
bool InitDlinkList(DLinkList &L){
L=(DNode*)malloc(sizeof(DNode)); //分配一个头结点
if(L==NULL)
return false;
L->prior=L;
L->next=L;
return true;
}
//判断循环双链表是否为空
bool Empty(DLinkList L){
if(L->next==L)
return true;
else
return false;
}
采用循环双链表时,删除,插入等操作,对于最后一个结点没有特殊
判空操作是遍历的while循环核心
静态链表
def:
- 在内存中分配一整片连续的内存空间
- 每个结点包括数据元素,以及下一个结点的数组下标(游标)
- 0号结点充当头结点
- 游标充当指针
- 游标的值设为-1及表示没有后继节点了
用代码定义一个静态链表
#define MaxSize 10 //静态链表的最大长度
struct Node{ //静态链表结构类型的定义
Elemtype data; //存储数据元素
int next; //下一个元素的数组下标
};
void testSLinkList(){
struct Node a[MaxSize]; //数组a作为静态链表
//后续代码
}
or
#define MaxSize 10 //静态链表的最大长度
typedef struct Node{ //静态链表结构类型的定义
Elemtype data; //存储数据元素
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
void testSLinkList(){
SLinkList b; //数组a作为静态链表
//后续代码
}
常用操作
- 查找:从头结点出发挨个往后遍历结点
- 插入位序为i的结点:1.找到一个空的结点,存入数据元素 2.从头结点出发找到位序为i-1的结点 3.修改新结点的next 4.修改i-1号结点的next
初始化时,应将空闲结点设置为某个特殊的值,方便于寻找判断空闲结点
3.删除:1.从头结点出发找到新的前趋结点 2.修改前趋结点的游标 3.被删除后的结点next设置为-2
**静态链表:**用数组的方式实现链表
- 优点:增,删操作不需要大量移动元素
- 缺点:不能随机存取,只能从头结点开始依次往后查
容量固定不变
适用场景
- 不支持指针的低级语言
- 数据元素容量固定不变的场景(如操作系统的文件分配表FAT)
顺序表与链表的对比
- 逻辑结构:都属于线性表,是线性结构
- 存储结构:顺序表–》顺序存储 :支持随机存取,存储密度高,大片连续内存分配不方便,改变容量不方便
链表----》链式存储:离散的小空间分配方便,改变容量方便,不可随机存取,存储密度低
3.基本操作:初始化(创),销(毁),增删改查
表长难以估计,经常增加/删除元素 ----链表
表长可估计,查询(搜索)操作较多 ----顺序表