小肥柴慢慢手写数据结构(C篇)(2-6 双链表 DoubleLinkedList)
目录
2-20 双链表的概念
2-20-1 为什么需要双链表
还是那句话:任何数据结构都有自己诞生的特定场合,是对某种指定功能的特型优化!脱离应用场景单独讨论某个数据结构是不合适的。
那么为什么会产生链表呢?
(1)某些场景下我们希望获得一个节点的前置节点(前驱);
(2)双保险,保证一个前后两个链接在断开一个或者一个异常的情况下仍能恢复;
(3)相比带head和tail的单链表,查询效率可以更高:相当于从两头开始查找……
2-20-2 双链表样式
(1)left由head开始,直到tail。
(2)right由tail开始,直到head。
(3)很多具体实现中也直接用prev表示left,next表示right,本质上都是相同的。
2-21 朴素的ADT及实现
对照单链表实现和上一贴2-5跳表实现,可以轻松的写出具体实现代码
我们依旧规定以下self版本的双向链表中,每个元素仅出现一次,且按照从小到大的顺序排列,仅给出了一些最基本的核心操作。
(1)list.h
#ifndef DU_LIST_H
#define DU_LIST_H
typedef int ElementType;
typedef struct Node{
ElementType data;
struct Node *left;
struct Node *right;
} Node;
typedef struct DuList{
int size;
Node *head;
Node *tail;
} DuList;
DuList *createDuList(); //空链表,有head和tail
Node *createNode(ElementType data); //生成节点
int insertNode(DuList *list, ElementType data); //按照顺序插入节点
Node *findNode(DuList *list, ElementType data); //查询节点
Node *removeNode(DuList *list, ElementType data); //删除节点
void printList(DuList *list); //打印
#endif
(2)list.c
其实无论插入和删除,只要对单链表的操作非常熟悉,也就是添加一次不同方向的对应操作而已,并没有那么可怕;但需要注意我们不在要求找到前置节点了,因为有两个方向的指针,非常方便。
#include <stdio.h>
#include <stdlib.h>
#include "list.h"
DuList *createDuList(){
DuList *list = (DuList*)malloc(sizeof(DuList));
if(!list)
return NULL;
list->head = createNode(INT_MIN);
list->tail = createNode(INT_MAX);
if(!list->head || !list->tail){
free(list);
return NULL;
}
list->head->left = NULL;
list->head->right = list->tail;
list->tail->left = list->head;
list->tail->right = NULL;
list->size = 0;
return list;
}
Node *createNode(ElementType data){
Node *newNode = (Node*)malloc(sizeof(Node) + 2*sizeof(Node*));
if(!newNode)
return NULL;
newNode->data = data;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
int insertNode(DuList *list, ElementType data){
if(!list)
return 1; //链表为空,报错码
Node *cur = list->head;
while(cur->data != INT_MAX && cur->data < data)
cur = cur->right;
if(cur->data == data)
return 2; //已经存在元素,不做插入动作
Node *newNode = createNode(data);
newNode->left = cur->left;
newNode->right = cur;
cur->left->right = newNode;
cur->left = newNode;
list->size++;
return 0;
}
Node *findNode(DuList *list, ElementType data){
if(!list)
return NULL;
Node *low = list->head; //从两头同时开始寻找,效率相对提升
Node *high = list->tail;
while(low->data <= high->data){
if(low->data == data)
return low;
if(high->data == data)
return high;
low = low->right;
high = high->left;
}
return NULL;
}
Node *removeNode(DuList *list, ElementType data){
Node *target = findNode(list, data);
if(target){
Node *left = target->left;
Node *right = target->right;
left->right = right;
right->left = left;
target->left = NULL; //文明返回节点
target->right = NULL; //断开左右两端
list->size--;
}
return target;
}
void printList(DuList *list){
printf("\nhead->");
Node *cur = list->head->right;
while(cur!=list->tail){
printf("[%d]->", cur->data);
cur = cur->right;
}
printf("tail");
}
(3)main.c
#include <stdio.h>
#include <stdlib.h>
#include "list.h"
int main(int argc, char *argv[]) {
DuList *list = createDuList();
int i;
printf("\n=======test insert && init=======");
for(i=0; i<5; i++)
insertNode(list, i);
printList(list);
for(i=6; i<10; i++)
insertNode(list, i);
printList(list);
insertNode(list, -1);
printList(list);
insertNode(list, -10);
printList(list);
insertNode(list, 5);
printList(list);
insertNode(list, 0);
printList(list);
printf("\n=======test find=======");
int arr[] = {10, -1, 0, 200};
int len = sizeof(arr)/sizeof(arr[0]);
for(i = 0; i < len; i++)
printf("\n find %d %s", arr[i], findNode(list, arr[i]) == NULL ? "fail" : "OK");
printf("\n=======test remove=======");
for(i = 0; i < len; i++)
printf("\n remove %d %s", arr[i], removeNode(list, arr[i]) == NULL ? "fail" : "OK");
printList(list);
return 0;
}
注意:
(1)因为已经有两个方向的指针了,对双链表讨论反转问题是没有意义的,小心别被面试官坑到了。
(2)有的朋友讨论并实现了双循环链表,个人感觉也仅仅是一种拓展和训练,实用性不强。
2-22 严版教材的相关讨论
位置:严版教材P35 2.3.3 双向链表
我估计作者的本意就是想要读者自己实践一遍,这里使用了prev/next替代left/right,代码虽然简洁,但是可读性不友好。
2-23 黑皮书的相关讨论
也仅仅是寥寥几字,作者认为这种升级/特型化的数据结构,是大家能够自行摸索掌握的,也就没有深挖了。
2-24 Linux链表源码解析(重点!)
照旧翻阅3.3.9的Linux源码(include\linux\list.h)
(1)基本形式为一个双链表
struct list_head {
struct list_head *next, *prev;
};
(2)初始化分为静态和动态两种情况
/*静态初始化*/
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
/*动态初始化*/
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
(3)头插和尾插
/**
* list_add - add a new entry
* @new: new entry to be added
* @head: list head to add it after
*
* Insert a new entry after the specified head.
* This is good for implementing stacks.
*/
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
/**
* list_add_tail - add a new entry
* @new: new entry to be added
* @head: list head to add it before
*
* Insert a new entry before the specified head.
* This is useful for implementing queues.
*/
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
标准的插入操作 __list_add( )
/*
* Insert a new entry between two known consecutive entries.
*
* This is only for internal list manipulation where we know
* the prev/next entries already!
*/
#ifndef CONFIG_DEBUG_LIST
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
#else
extern void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next);
#endif
(4)删除节点
/*
* Delete a list entry by making the prev/next entries
* point to each other.
*
* This is only for internal list manipulation where we know
* the prev/next entries already!
*/
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
/**
* list_del - deletes entry from list.
* @entry: the element to delete from the list.
* Note: list_empty() on entry does not return true after this, the entry is
* in an undefined state.
*/
#ifndef CONFIG_DEBUG_LIST
static inline void __list_del_entry(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
}
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
#else
extern void __list_del_entry(struct list_head *entry);
extern void list_del(struct list_head *entry);
#endif
(5)遍历操作
/**
* list_for_each - iterate over a list
* @pos: the &struct list_head to use as a loop cursor.
* @head: the head for your list.
*/
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
其实还有很多的别的类似遍历操作
/**
* list_for_each_safe - iterate over a list safe against removal of list entry
* @pos: the &struct list_head to use as a loop cursor.
* @n: another &struct list_head to use as temporary storage
* @head: the head for your list.
*/
#define list_for_each_safe(pos, n, head) \
for (pos = (head)->next, n = pos->next; pos != (head); \
pos = n, n = pos->next)
/**
* list_for_each_prev - iterate over a list backwards
* @pos: the &struct list_head to use as a loop cursor.
* @head: the head for your list.
*/
#define list_for_each_prev(pos, head) \
for (pos = (head)->prev; pos != (head); pos = pos->prev)
……
(7)以上遍历方式仅可用于查询节点的当前位置,想获取节点本身的数据结构,需要用list_entry()
/**
* list_entry - get the struct for this entry
* @ptr: the &struct list_head pointer.
* @type: the type of the struct this is embedded in.
* @member: the name of the list_struct within the struct.
*/
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
对应的container_of()宏在kernel.h中
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
参考资料[3]中的解释:其中offsetof()宏是通过把0地址转换为type类型的指针,然后去获取该结构体中member成员的指针,也就是获取了member在type结构体中的偏移量。最后用指针ptr减去offset,就得到type结构体的真实地址了。
这段代码看明白了,说明你对指针的理解还是比较透彻的。
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
以上代码在陈莉君老师的《Linux操作系统原理与应用》的简化版教材中特别提出来,确实非常经典,相比我们自己的实现和严版教材的提示都要优秀!
如果想尝试使用可以参考他人帖子:linux内核链表list_head
2-25 小结
双链表的使用是要看场合的,在Linux中就把这个数据结构给工具化了,而且确实很好用;我们自己实现的版本这个过程非常重要,但在实际应用中还是直接使用大佬们的成品吧。
下一贴将讨论多项式问题,这个经典的问题在很多高校的数据结构课程中都提到过,清华版、浙大版和哈工大版各有自己的特色。
参考资料
[1] 双链表-详细解释,图文并茂
[2] Linux中的经典双链表的实现
[3] Linux内核中常用的数据结构和算法
[4] linux内核链表list_head