双向链表与单向链表的区别
上一篇中我们说过了单向链表,今天我们来看一下双向链表,看看它与单向链表的区别及双向链表的创建、插入、删除等操作。
双向链表的定义
//定义结点
typedef struct Node{
ElemType data;
struct Node *prior; // 较单向链表多出的前指针域
struct Node *next;
}Node;
typedef struct Node * LinkList;
双向链表的创建
创建双向链表一般也会先创建一个空的头结点,便于后续对链表的操作。
- 先创建一个头结点,判定创建是否成功
- 预设链表一个结点的个数,采用后插法增加链表结点
- 确定结点之间的双向链表关系,记录最新一个结点的位置
Status createDoubleLinkList(linkList *L){
// 创建一个空结点
*L = (linkList)malloc(sizeof(Node));
if (*L == NULL) return ERROR;
(*L)->prior = NULL;
(*L)->next = NULL;
(*L)->data = -1; // 值没有意义,因为在后面打印的时候忽略打印
// 使用一个临时指针最新的结点的位置,方便后续插入新的结点
linkList p = *L;
// 新增结点
int maxNodeCount = 10;
for (int i =0; i < maxNodeCount; i ++) {
linkList temp = (linkList)malloc(sizeof(Node));
if (temp == NULL) {
return ERROR;
}
temp->data = i; // 可用其他赋值
temp->prior = NULL;// 初始设为NULL,后期可以重新设定
temp->next = NULL;
// temp和P的关系:p的next指向temp,temp的prior指向p
p->next = temp;
temp->prior = p;
// p记录最新一个的结点temp
p = temp;// 或者写成p = p->next,但是个人感觉可读性不如p = temp
}
return OK;
}
打印双向链表
void display(LinkList L){
// 空的头结点不打印,从下一个结点开始打印
LinkList temp = L->next;
if(temp == NULL){
printf("打印的双向链表为空!\n");
return;
}
while (temp) {
printf("%d ",temp->data);
temp = temp->next;
}
printf("\n");
}
双向链表的插入
- 判断插入的位置是否合法,place不能小于1
- 找到要插入位置place的的结点
- 新增一个临时结点temp
- 重新确定结点之间的链接关系
- 注意:一定要先将后一个结点B和临时结点temp的关系先确定,否则结点B容易丢失
Status insertDoubleLinkList(linkList *L,int place,ElemType data)
{
// 判断插入位置是否合法
if (place < 1) {
return ERROR;
}
// 指向头结点
linkList p = *L;
// 找到插入位置place的结点
for (int i = 1; i < place && p; i ++) {
p = p->next;
}
if (p == NULL) {
return ERROR;
}
// 新增一个新节点
linkList temp = (linkList)malloc(sizeof(Node));
if (temp == NULL) {
return ERROR;
}
temp->data = data;
temp->prior = NULL;
temp->next = NULL;
// temp 插入在place的后面,就是p的后面
// 如果p是链表尾部结点,那么p->next就是插入的temp
if (p->next == NULL) {
p->next = temp;
temp->prior = p;
} else {
// 1 要先将p的下一个结点B的prior指向temp,temp的next指向B
linkList B = p->next;
B->prior = temp;
temp->next = B;
// 2 再将P的next指向temp,temp的prior指向P
p->next = temp;
temp->prior = p;
// 如果将1,2 的操作顺序弄反,会造成结点B丢失
}
return OK;
}
双向链表删除
- 判断链表合法性,如果是空的链表,删除也就没有意义了
- 找到要删除位置place的前一个结点P
- 修改P的next指针指向要删除的结点delTemp的next也就是图中的结点C
- 修改结点C的prior指向P
- 释放要删除的结点delTemp
Status deleteLinkList(linkList *L,int place,ElemType *deleteDate)
{
// 判断链表的合法性
if (*L == NULL) {
return ERROR;
}
linkList p = *L;
// 找到要删除位置的前一个结点P
for (int i = 1; i < place && p; i ++) {
p = p -> next;
}
if (p == NULL) {
return ERROR;
}
// 创建临时指针指向要删除的结点
linkList delTemp = p->next;
*deleteDate = delTemp->data;
linkList C = delTemp->next;
p->next = C;
// 如果删除的是最后一个结点即c为空,那么c就不能把prior指向p,否则c的prior指向P
if (C != NULL) {
C->prior = p;
}
// 释放要删除的结点
free(delTemp);
return OK;
}
双向链表删除指定元素
Status LinkListDeletVAL(LinkList *L, int data){
LinkList p = *L;
//1.遍历双向循环链表
while (p) {
//2.判断当前结点的数据域和data是否相等,若相等则删除该结点
if (p->data == data) {
//修改被删除结点的前驱结点的后继指针,参考图上步骤1️⃣
p->prior->next = p->next;
//修改被删除结点的后继结点的前驱指针,参考图上步骤2️⃣
if(p->next != NULL){
p->next->prior = p->prior;
}
//释放被删除结点p
free(p);
// 如果只想删除一个指定元素,break跳出循环
// 想要删除所有相同的元素就不用break
break;
}
//没有找到该结点,则继续移动指针p
p = p->next;
}
return OK;
}
以上就是双向链表的相关操作。双向链表的头结点的prior为NULL,尾结点的next为NULL,其他结点的prior和next各有结点链接。
当我们把头结点的prior指向尾结点,尾结点的next指向头结点,这样就形成了一个双向循环链表了。好,我们以下就来介绍双向循环链表。
双向循环链表的创建,添加,删除等操作可参考双向链表对应的操作,就不继续分析了。
总结
数据结构的存储一般常用包括:顺序存储结构和链式存储结构。
-
空间分配:
顺序结构存储数据,需预先申请一整块足够大的存储空间,然后将数据按照次序逐一存储,数据之间紧密贴合,不留一丝空隙。链表的存储方式与顺序表截然相反,什么时候存储数据,什么时候才申请存储空间,数据之间的逻辑关系依靠每个数据元素携带的指针维持。
-
存储密度
顺序表存储数据实行的是 "一次开辟,永久使用",即存储数据之前先开辟好足够的存储空间,空间一旦开辟后期无法改变大小(使用动态数组的情况除外)。链表则不同,链表存储数据时一次只开辟存储一个节点的物理空间,如果后期需要还可以再申请。
-
操作效率
链表中数据元素之间的逻辑关系靠的是节点之间的指针,当需要在链表中某处插入或删除节点时,只需改变相应节点的指针指向即可,无需大量移动元素,因此链表中插入、删除或移动数据所耗费的时间复杂度为 O(1)。顺序表中,插入、删除和移动数据可能会牵涉到大量元素的整体移动,因此时间复杂度至少为 O(n)。
-
访问效率
顺序表中存储的元素可以使用数组下标直接访问,无需遍历整个表,因此使用顺序表访问元素的时间复杂度为 O(1)。链表中访问数据元素,需要从表头依次遍历,直到找到指定节点,花费的时间复杂度为 O(n);