小肥柴慢慢手写数据结构(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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值