数据结构——链表

链表(Linked list)

1 相关概念

链表是一种常用的数据结构,它通过指针将一些列数据结点,连接成一个数据链。

优点:

  1. 动态数据结构:相对于数组,具有更好的动态性(非顺序存储);
  2. 内存利用率高:建立链表时无需预先知道数据总量大小,可随机的分配空间;链表大小可在程序运行时增加或减少;
  3. 易于插入和删除

缺点:

  1. 内存使用:数组只需要数据域空间,链表每个节点都包含指针域,其需要额外的内存空间;
  2. 遍历困难:链表随机访问的性能没有数组好,需要 O(n) 的时间复杂度;

链表数组性能对比

img

常见的链表种类:

  • 单链表

img

  • 双向链表

img

  • 循环链表

img

  • 带头链表
  • 不带头链表
  • 静态链表:初始长度一般是固定的,在做插入和删除操作时不需要移动元素,仅需修改指针,故仍具有链式存储结构的主要优点
  • 动态链表:其相对于静态链表而言的,一般地,在描述线性表的链式存储结构时如果没有特别说明即默认描述的是动态链表

2 链表的相关操作

2.1 单向链表

头文件singly_list.h

#ifndef _SINGLY_HEAD_H_
#define _SINGLY_HEAD_H_
 
#include <stdio.h>
#include <string.h>
#include <stdlib.h>


//链表的节点
typedef struct node
{
	int data;	//数据域
	struct node *next;	//指针域
}node;

//初始化一个有头链表的头结点
int init_list(node *head_p);

//销毁一个节点
void free_node(node *node);

//从尾部插入节点
int insert_node(node *head, int data);

//遍历一个带头节点的链表
void print_list(node *head);

//查找一个链表的节点
node *search(int data, node *head);

//删除带头结点的元素
int delete_node(node *del_node, node* head);

// 销毁一个链表
int destory_list(node *head_p);

#endif


函数体singly_list.c

#include "singly_list.h"

// 初始化链表
int init_list(node *head_p)
{
	head_p->data = 0;
	head_p->next = NULL;

	return 0;
}

// 从尾部插入节点
int insert_node(node *head, int data)
{
	node *last_node = NULL;
	node *new_node = NULL;
	
	if (head == NULL) {
		return -1;
	}
	// 指向最后一个节点
	for (last_node = head; last_node->next != NULL; last_node = last_node->next);

	new_node = (node *)malloc(sizeof(node));
	if (new_node == NULL) {
		printf("malloc error\n");
		return -1;
	}

	new_node->data = data;
	new_node->next = last_node->next;
	last_node->next = new_node;

	return 0;
}

// 遍历一个带头节点的链表
void print_list(node *head)
{
	node *p = NULL;

	for (p = head->next; p != NULL; p = p->next) {
		printf("data: %d\n", p->data);
	}
}

// 查找一个链表的节点
node *search(int data, node *head)
{
	node *tmp = NULL;

	for (tmp = head->next; tmp != NULL; tmp = tmp->next) {
		if (tmp->data == data) {
			return tmp;
		}
	}

	return NULL;
}


// 销毁一个节点
void free_node(node *node)
{
	if (node != NULL) {
		free(node);
	}
}



// 删除带头结点的元素
int delete_node(node *del_node, node* head)
{
	node *tmp = NULL;

	if (del_node == NULL || head == NULL) {
		return -1;
	}

	for (tmp = head; tmp->next != NULL; tmp = tmp->next) {
		if (tmp->next == del_node) {
			//找到了被删除的节点 就是p->next
			tmp->next = tmp->next->next;
			free_node(del_node);
			break;
		}
	}

	return 0;
}


// 销毁一个链表
int destory_list(node *head_p)
{
	node * tmp = NULL;
	node *head = NULL;

	if (head_p == NULL) {
		return -1;
	}

	head = head_p;

	for (tmp = head->next; tmp != NULL;) {
		// 删除节点
		head->next = tmp->next;
		free_node(tmp);
		tmp = head->next;
	}

	return 0;
}

主函数main.c

#include "singly_list.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>


int main(void)
{
	node head ;
	node *new_node;
 	node *tmp_node;
	int num = 10;
	int data = 0;
 	int status = 0;
	status = init_list(&head);
  if (0 == status)
  {
      printf("init_list ok\n");
  }

	for (int i = 0; i < num; i++) {
		insert_node(&head, i + 10);
	}
	print_list(&head);


  tmp_node = search(15, &head);
  printf("%d\n", tmp_node->data);

  status = delete_node(tmp_node, &head);
  if (0 == status)
  {
      printf("delete_node ok\n");
  }

  print_list(&head);

  status = destory_list(&head);
  if (0 == status)
  {
      printf("destory_list ok\n");
  }

	return 0;
}

执行效果:

在这里插入图片描述

2.2 双向链表

头文件doubly_list.h

#ifndef _D_LIST_H_
#define _D_LIST_H_


#include <stdio.h>
#include <string.h>
#include <stdlib.h>


typedef struct node
{
	int data;           //数据域
	struct node *next;  //下一个节点
	struct node *prev;  //上一个节点
}node;


//初始化一个双向链表
void init_list(node *head_p);

//创建一个节点
node *make_node(int data);

//销毁一个节点
void free_node(struct node *node);

//插入一个节点(m末尾)
int insert_node(node *head, node *new_node);

//前插
int addTail_node(node *new_node, node *cur_head);

//尾插
int add_node(node *new_node, node *cur_head);

//顺序遍历
void print_list_1(node *head);

//查找一个节点
node *search(node *head, int data);

//删除一个双向链表的节点
int delete_node(node*head, node *del_node);

//销毁一个双向链表
int destory_list(node *head_p);

#endif

函数体doubly_list.c

#include "doubly_list.h"


// 初始化一个双向链表
void init_list(node *head_p)
{
	head_p->data =  0;
	head_p->next = NULL;
	head_p->prev = NULL;

	return;
}

//创建一个节点
node *make_node(int data)
{
	node *tmp = NULL;

	tmp = (node *)malloc(sizeof(node));
	if (tmp == NULL) {
		return NULL;
	}
	tmp->data = data;
	tmp->next = tmp->prev = NULL;

	return tmp;
}

//销毁一个节点
void free_node(node *list)
{
	if (list != NULL) {
		free(list);
	}
}

//插入一个节点
int insert_node(node *head, node *new_node)
{
	node *last_node = NULL;
	if (head == NULL || new_node == NULL) {
		return -1;
	}
	// 指向最后一个节点
	for (last_node = head; last_node->next != NULL; last_node = last_node->next);

	//改变new_node 的自身的指针
	new_node->next = last_node->next;
	new_node->prev = last_node;

	//改变last_node 
	last_node->next = new_node;

	return 0;
}

void _listAdd(node *newlist, node *prev, node *next)
{

	next->prev 		= newlist;
	newlist->next 	= next;
	newlist->prev 	= prev;
	prev->next 		= newlist;

}

// 将new_node节点插入到cur_head和cur_head->next之间
int addTail_node(node *new_node, node *cur_head)
{
	if (cur_head == NULL || new_node == NULL) {
		return -1;
	}
	_listAdd(new_node, cur_head, cur_head->next);

	return 0;
}

// 将new_node节点插入到cur_head->prev,和cur_head之间
int add_node(node *new_node, node *cur_head)
{
	if (cur_head == NULL || new_node == NULL) {
		return -1;
	}
	_listAdd(new_node, cur_head->prev, cur_head);

	return 0;
}

//遍历一个链表
void print_list_1(node *head)
{
	node *tmp = NULL;

	for (tmp = head->next; tmp != NULL; tmp = tmp->next) {
		printf("data: %d cur=%p prev=%p next=%p\n", tmp->data, tmp, tmp->prev, tmp->next);
	}
}

//查找一个节点
node *search(node *head, int data)
{
	node *tmp = NULL;

	for (tmp = head->next; tmp != NULL; tmp = tmp->next) {
		if (tmp->data == data) {
			return tmp;
		}
	}

	return NULL;
}

//删除一个双向链表的节点
int delete_node(node*head, node *del_node)
{
	node *p = NULL;

	for (p = head->next; p != NULL; p = p->next) {
		//遍历链表中除了 head 和tail的每一个元素
		if (p == del_node) {
			//p是删除的节点
			//应该改变 p的前驱节点 和 p的后继节点 , p本身的两个指针不要动
			p->next->prev = p->prev;
			p->prev->next = p->next;
			free_node(p);
			break;
		}
	}

	return 0;
}

//销毁一个双向链表
int  destory_list(node *head_p)
{
	
	if (head_p == NULL ) {
		return -1;
	}

	node *p = NULL;

	for (p = head_p->next; p->next != NULL; ) {
		p->next->prev = p->prev;
		p->prev->next = p->next;
		free_node(p);
		
	
		p = head_p->next;
		printf("w%d\n", p->data);
			if ( p == NULL || p->next == NULL)printf("OK\n");
		
	}
	//以上就删除了 除了head 和tail的全部元素
	if (head_p->next->next == NULL) {
		printf("此时链表已经空 除了(head , tail)\n");

		free_node(head_p->next);

	}
	return 0;
}



3 Linux内核——list.h

看了Linux内核实现的链表操作,我直呼好家伙,这就是和大佬之间不可逾越的差距。

核心设计思路:只包含有指针域,不包含数据域!!!

3.1 初始化

  • 定义
struct list_head {
	struct list_head *next, *prev;
};
  • 初始化

其初始化了一个双向循环链表。list为头指针

// 初始化方法1
#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)
// 初始化方法2
static inline void INIT_LIST_HEAD(struct list_head *list)
{
	list->next = list;
	list->prev = list;
}

上面的宏可以简化为:

struct list_head name = { &(name), &(name) }

例子1:

int main(void)
{
	// 使用宏定义来初始化
	LIST_HEAD(list_1);
	printf("prev :%p next :%p\n", list_1.prev, list_1.next);
	printf("=============\n");
	// 使用函数来实现初始化
	struct list_head list_2;
	INIT_LIST_HEAD(&list_2);
	printf("prev :%p next :%p\n", list_2.prev, list_2.next);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ARuJkoO2-1633154427720)(C:\Users\zxm\Desktop\笔记\csdn\链表.assets\image-20211001215957468.png)]

例子2:

用户在使用过程中只需要定义自己需要用到的数据域,其链表结构通过list_head进行串起来。这样的结构大大的提高代码复用的效率!!!

typedef struct userDef
{
	/* 数据域*/
	int num;
	char data[50];
	/*指针域*/
	struct list_head list;
}userDef;

int main(void)
{
	userDef myList;
	INIT_LIST_HEAD(&myList.list);
	printf("prev :%p next :%p\n", myList.list.prev, myList.list.next);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A4G3zzIw-1633154427725)(C:\Users\zxm\Desktop\笔记\csdn\链表.assets\新建 Microsoft Visio Drawing.jpg)]

3.2 数据的访问机制

注:其访问机制通过下面的宏定义实现

#define list_entry(ptr, type, member) \
	container_of(ptr, type, member)

#define container_of(ptr, type, member) ({          \
	const typeof(((type *)0)->member)*__mptr = (ptr);    \
	(type *)((char *)__mptr - offsetof(type, member));})

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

这一大串宏定义着实看起来有点懵逼,首先:

1、container_of

container_of的实现机制该博文中进行了详细的介绍。
其作用为:通过结构体type的成员member的地址ptr,求解得到解结构体type的起始地址。

type的起始地址 = ptr - size(其中ptr的地址已知;未知参数为size,为TYPE类型对象中MEMBER成员的地址偏移量;)

2、第一条语句:const typeof(((type )0)->member)__mptr = (ptr);

  • 首先将0x0转换为TYPE类型的指针变量,再访问member成员
  • typeof(x)函数返回的是x的数据类型,即member的数据类型
  • __mptr 的数据类型为:member成员的数据类型的指针,再将ptr赋值给它

**可能大家会问为什么不直接使用ptr这个指针,为什么还需要定义一个中间变量呢?**其主要为了解决ptr与member类型不匹配,编译时便会有warnning的问题。

3、第二条语句:(type *)((char *)__mptr - offsetof(type, member));

  • 先将__mptr强制转换为char*类型

求解size

对于size的求解通过 #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 这个宏定义实现

  • 将0x0地址强制转换为TYPE*类型,然后取TYPE中的成员MEMBER地址,因为 起始地址为0,得到的MEMBER的地址就直接是该成员相对于TYPE对象的偏移地址了,即未知参数size

4、最后宏:#define list_entry(ptr, type, member) container_of(ptr, type, member) 得到返回type类型对象的地址。

3.3 遍历链表

基于前面所提的随机访问机制,链表的遍历就变的容易很多了

在这里插入图片描述

  • 访问第一个节点
#define list_first_entry(ptr, type, member) \
	list_entry((ptr)->next, type, member)
  • 访问最后一个节点
#define list_last_entry(ptr, type, member) \
	list_entry((ptr)->prev, type, member)
  • 遍历所有对象

在这里插入图片描述

#define list_for_each_entry(pos, head, member)				\
for (pos = list_entry((head)->next, typeof(*pos), member);	\
	prefetch(pos->member.next), &pos->member != (head); 	\
	pos = list_entry(pos->member.next, typeof(*pos), member))
/*
实际上就是一个for循环,循环内部由用户自己根据需要定义
初始条件:pos = list_entry((head)->next, typeof(*pos), member); pos为第一个对象地址
终止条件:&pos->member != (head);pos的链表成员不为头指针,环状指针的遍历终止判断
递增状态:pos = list_entry(pos->member.next, typeof(*pos), member) pos为下一个对象地址
所以pos就把挂载在链表上的所有对象都给遍历完了。至于访问对象内的数据成员,有多少个,用来干嘛用,则由用户
根据自己需求来定义了。
*/

3.4 添加元素

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IoWCGDpl-1633154427729)(C:\Users\zxm\Desktop\笔记\csdn\链表.assets\124.jpg)]

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;
}
//head是头指针
//头节点后添加元素,相当于添加头节点,可实现栈的添加元素
//new添加到head之后,head->next之前
static inline void list_add(struct list_head *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}
 

//头节点前添加元素,因为是环状链表所以相当于添加尾节点,可实现队列的添加元素
//new添加到head->prev之后,head之前
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
	__list_add(new, head->prev, head);
}

3.5 删除元素

//prev和next两个链表指针构成双向链表关系
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;
	prev->next = next;
}
 
//删除指定元素,这是一种安全删除方法,最后添加了初始化
static inline void list_del_init(struct list_head *entry)
{
	//删除entry节点
	__list_del(entry->prev, entry->next);
	//entry节点初始化,是该节点形成一个只有entry元素的双向链表
	INIT_LIST_HEAD(entry);
}

3.6 替换元素

//调整指针,使new的前后指针指向old的前后,反过来old的前后指针也指向new
//就是双向链表的指针调整
static inline void list_replace(struct list_head *old,
                                struct list_head *new)
{
	new->next = old->next;
	new->next->prev = new;
	new->prev = old->prev;
	new->prev->next = new;
}
 
//替换元素,最后将old初始化一下,不然old还是指向原来的两个前后节点(虽然人家不指向他)
static inline void list_replace_init(struct list_head *old,
                                     struct list_head *new)
{
	list_replace(old, new);
	INIT_LIST_HEAD(old);
}

3.7 移动元素

//移动指定元素list到链表的头部
static inline void list_move(struct list_head *list, struct list_head *head)
{
	__list_del(list->prev, list->next);//删除list元素
	list_add(list, head);//将list节点添加到head头节点
}
 
/**
* list_move_tail - delete from one list and add as another's tail
* @list: the entry to move
* @head: the head that will follow our entry
*/
//移动指定元素list到链表的尾部
static inline void list_move_tail(struct list_head *list,
                                  struct list_head *head)
{
	__list_del(list->prev, list->next);
	list_add_tail(list, head);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值