简单理解
我们会发现,单链表由于只有next域,所以,如果想要访问某个元素的前驱结点,那么只能从头开始遍历到该元素的前一个元素。效率非常的低下。
于是,为了方便的访问前驱、后继,双向链表应运而生。
如上图所示,就是一个简单的双向链表示意图。
双向链表共含有三个元素:
1、存放数据;
2、后继指针域;
3、前驱指针域。
增删图解
双向链表的插入和删除操作需要特别注意边界问题。
正常的插入如下图:
插入情景:将新结点 p 插入到指定结点 q 的后面
:
看上去这四个步骤好像没有什么问题,但是,步骤2在处理边界问题的时候容易崩溃,原因如下图:
故,正确的插入操作需要对步骤2添加条件判断:
p->next = q->next;
if (p->next != NULL)
{
p->next->prio = p;
}
q->next = p;
p->prio = q;
删除操作原理同上,在此就不多做赘述了。
我们来看代码:
头文件
头文件命名为:dlist.h
#pragma once
// 带头结点的双向链表,不循环
typedef struct DNode
{
int data;
struct DNode* next;// 后继指针
struct DNode* prio;// 前驱指针
}DNode,*DList;
// 初始化函数
void InitList(DList plist);
// 判空
bool IsEmpty(DList plist);
// 获取数据长度
int GetLength(DList plist);
// 头插
bool Insert_head(DList plist, int val);
// 尾插
bool Insert_tail(DList plist, int val);
// 在plist中查找关键字key,找到返回目标地址,失败返回NULL
DList Search(DList plist, int key);
// 删除plist中的第一个key
bool DeleteVal(DList plist, int key);
// 打印输出所有数据
void Show(DList plist);
// 逆置
void Reverse(DList plist);
// 清空数据
void Clear(DList plist);
// 销毁动态内存
void Destroy(DList plist);
代码文件
代码文件命名为:dlist.cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "dlist.h"
// 初始化函数
void InitList(DList plist)
{
assert(plist != NULL);
if (plist == NULL) return;
plist->next = NULL;
plist->prio = NULL;
}
// 判空
bool IsEmpty(DList plist)
{
assert(plist != NULL);
return plist->next == NULL;
}
// 获取数据长度
int GetLength(DList plist)
{
assert(plist != NULL);
int count = 0;
for (DNode* p = plist->next; p != NULL; p = p->next)
{
count += 1;
}
return count;
}
// 头插
bool Insert_head(DList plist, int val)
{
assert(plist != NULL);
DNode* p = (DNode*)malloc(sizeof(DNode));
p->data = val;
// 将p插在头结点的后面
p->next = plist->next;
// 特别注意,这里需要加判断
if (p->next != NULL)
{
// 如果是第一次插入结点,前面p->next = plist->next == NULL;
// 此时p->next->prio <==> NULL->prio,会发生访问冲突
p->next->prio = p;
}
plist->next = p;
p->prio = plist;
return true;
}
// 尾插
bool Insert_tail(DList plist, int val)
{
assert(plist != NULL);
DNode* p = plist;
while (p->next != NULL)
{
p = p->next;
}
DNode* q = (DNode*)malloc(sizeof(DNode));
q->data = val;
// 将q插入在尾结点p的后面
q->next = p->next;
p->next = q;
q->prio = p;
return true;
}
// 在plist中查找关键字key,找到返回目标地址,失败返回NULL
DList Search(DList plist, int key)
{
assert(plist != NULL);
for (DNode* p = plist->next; p != NULL; p = p->next)
{
if (key == p->data)
{
return p;
}
}
return NULL;
}
// 删除plist中的第一个key
bool DeleteVal(DList plist, int key)
{
assert(plist != NULL);
DNode* p = Search(plist, key);
if (p == NULL)
{
return false;
}
// 将p从链表中剔除
p->prio->next = p->next;
if (p->next != NULL)
{
p->next->prio = p->prio;
}
free(p);
return true;
}
// 打印输出所有数据
void Show(DList plist)
{
assert(plist != NULL);
for (DNode* p = plist->next; p != NULL; p = p->next)
{
printf("%d ",p->data);
}
printf("\n");
}
// 逆置
void Reverse(DList plist)
{
assert(plist != NULL);
// p定位到最后一个数据结点
DNode* p = plist->next;
while (p->next != NULL)
{
p = p->next;
}
// 处理头结点
plist->next = p;
// 处理所有的next域
while (p->prio != plist)
{
p->next = p->prio;
p = p->prio;
}
if(p->prio == plist)
{
p->next = NULL;
}
// 接着处理prio域
p = plist->next;
DNode* q = plist;
while (p != NULL)
{
p->prio = q;
p = p->next;
q = q->next;
}
}
// 清空数据
void Clear(DList plist)
{
Destroy(plist);
}
// 销毁动态内存
void Destroy(DList plist)
{
assert(plist != NULL);
DNode* p;
while (plist->next != NULL)
{
p = plist->next;
plist->next = p->next;
free(p);
}
}
测试用例
用如下的测试代码来检验我们所写的程序:
#include <stdio.h>
#include <iostream>
#include "dlist.h"
int main()
{
DNode head;
InitList(&head);
for (int i = 0; i < 7; i++)
{
Insert_tail(&head, i);
}
Show(&head);
Reverse(&head);
Show(&head);
return 0;
}
输出结果如下:
笔者这里着重检查了逆置函数,顺带检验了插入和打印函数,其他的没有在本测试代码中体现,有兴趣的可以自行验证。
参考资料
【1】严蔚敏. 数据结构(C语言版). 北京:清华大学出版社,2009:30.