1 前言: 前几天写过一篇单链表逆序的文章点击打开链接, 本来准备第二天就把双向链表方便的东西补全的,
奈何公(懒)务(癌)缠(发)身(作) , 拖到现在。
2 什么是链表
老规矩 , 先上一副数据结构的图 , 希望大家加深理解
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序通过链表中的指针链接次序实现。链表由一系列存储结点组成,结点可在运行时动态生成。每个结点均由两部分组成,即存储数据元素的数据域和存储相邻结点地址的指针域。当进行插入或删除操作时,链表只需修改相关结点的指针域即可,因此相比线性表顺序结构更加方便省时。
链表可分为单链表(Singly Linked List)、双向链表(Doubly Linked List)和循环链表(Circular List)等。一个单链表结点仅包含一个指向其直接后继结点的指针,因此当需要操作某个结点的直接前驱结点时,必须从单链表表头开始查找。
双向链表和循环链表均为单链表的变体。通常创建双向循环链表以综合利用两者的优点。
2.1 双向链表
双向链表的节点 , 除了有数据域之外 ,还有两个指针域, 分别指向该节点的前驱节点和后继节点,因此, 从双向链表中任意位置开始 , 都能很方便得访问其前驱和后继节点 , 节点如图1
图1 双向链表节点
如图1所示 , Data为该节点的数据域,prev指向直接前驱节点, 而next指向直接后继节点,通常, 双向链表会含有一个表头(也称作哨兵节点), 它的作用是能够迅速定位到链表头的位置, 简化我们对链表的任何操作包括但不限于增加,删除,遍历,查找等等操作, 双向有头非空链表结构如下图2
图2 双向有头非空链表
图中表头节点dhead的prev指向空, C节点的next指向空, 除表头节点和尾节点外, 链表中任意节点都满足以下关系
p = p->next->prev = p->prev->next
也就是 , 当前节点的前驱的后继是当前节点 , 当前节点的后继的前驱也是当前节点, 虽然有点拗口 , 但是请大家要记住。
3 双向循环链表实现:
3.1插入
假设指针p和q指向双向链表中的两个前后相邻结点,将某个新结点(指针为s)插到p和q之间,其过程及C语言描述如下图所示图3:
图3 双向链表插入过程
请大家注意 , 插入顺序不具备唯一性 , 但是为了保证链接的完整 , 第四步操作一定要是p->next 或 q->prev,不然会丢失掉P的后继或者是q的前驱 , 这里要一定要注意,总结下伪代码如下:
s->prev = p;
s->next = q;
q->prev = s;
p->next = s;
3.2 删除
删除也就是插入的逆向操作 , 如上图, s节点成功插入到链表中, 我们删除也只需改变s节点的前驱和后继指针的指向 , 再释放掉s节点申请的内存就行了, 伪代码如下:
p->next = s->next;
q->prev = s->prev;
free(s->data);
free(s);
3.3 查找
查找也就是从头节点开始, 遍历整张表, 遍历的时候逐一比对数据域的值就行了
代码如下:
llist.h
#ifndef ___LLIST_H__
#define ___LLIST_H__
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
//定义函数指针
typedef void (llist_op_t)(void *);
typedef int (llist_cmp_t)(void *, void *);
//获取字符串
#define GETLINES(string, buf) do{ \
printf(string); \
fgets(buf, sizeof(buf), stdin); \
if (buf[strlen(buf) - 1] == '\n') \
buf[strlen(buf) - 1] = '\0'; \
}while(0)
//