单链表的链式存储结构的结点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针向后寻查其他结点。若要寻查结点的直接前驱,则必须从表头指针出发。换句话说。在单链表中,查找直接后继结点的执行时间为0(1),而查找直接前驱的执行时间为0(n)。为克服单链表这种单向性的缺点,可利用双向链表(Double Linked List)。
顾名思义,在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱。
1.双链表的结构定义
typedef struct DuLNode {
ElemType data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLnode,*DuLinkList;
双向循环链表
和单链的循环表类似,双向链表也可以有循环表
- 让头结点的前驱指针指向链表的最后一个结点
- 让最后一个结点的后继指针指向头结点。
2.双链表结构的对称性
p->prior->next = p = p->next->prior
在双向链表中有些操作(如:istLength、GetElem等),因仅涉及一个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者的操作的时间复杂度均为O()。
2.双向链表的插入
- s->prior=p->prior;
- p->prior->next=s;
- s->next=p;
- p->prior=s;
void insertBefore(Node** head, Node* p, int value) {
Node* s = (Node*)malloc(sizeof(Node)); // 创建新节点
if (!s) return; // 内存分配失败处理
s->data = value; // 设置新节点的数据
s->next = p; // 设置新节点的后继为p
s->prior = p->prior; // 设置新节点的前驱为p的前驱
if (p->prior) { // 如果p不是头节点
p->prior->next = s; // 更新p的前驱的后继指向新节点
} else { // 如果p是头节点
*head = s; // 更新头指针
}
p->prior = s; // 更新p的前驱指向新节点
}
3.双向链表的删除
- p->prior-next=p->next;
- p->next->prior=p->prior;
void deleteNode(Node** head, Node* p) {
if (p == NULL) return; // 检查p是否为NULL
// 更新前驱节点的后继指针
if (p->prior != NULL) {
p->prior->next = p->next;
} else {
*head = p->next; // 如果p是头节点,更新头指针
}
// 更新后继节点的前驱指针
if (p->next != NULL) {
p->next->prior = p->prior;
}
// 释放被删除节点的内存
free(p);
}
4.顺序表和链表的比较
链式存储结构的优点:
- 结点空间可以动态申请和释放;
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。
链式存储结构的缺点: - 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
- 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的杂度。
存储结构
比较项目 | 顺序表 | 链表 |
---|---|---|
预先分配,会导致空间闲置或溢出现象 | √ | × |
动态分配,不会出现存储空间闲置或溢出现象 | × | √ |
不用为表示结点间的逻辑关系而增加额外的存储开销,存储密度等于1 | √ | × |
需要借助指针来体现元素间的逻辑关系,存储密度小于1 | × | √ |
随机存取,按位置访问元素的时间复杂度为O(1) | √ | × |
顺序存取,按位置访问元素时间复杂度为O(n) | × | √ |
平均移动约表中一半元素,时间复杂度为O(n),时间复杂度为O(1) | × | √ |
不需移动元素,确定插入、删除位置后,时间复杂度为O(1) | × | √ |
适用情况
- 顺序表:
- 表长变化不大,且能事先确定变化的范围
- 很少进行插入或删除操作,经常按元素位置序号访问数据元素
- 链表:
- 长度变化较大
- 频繁进行插入或删除操作