数据结构之双链表(双链表的创建头插法和尾插法详解)

一.前言

双链表的内容比较多,而且更加繁琐,我会分成几次来更新,所以会更的很慢,老实说我自己也不清楚能不能把数据结构更完,我写这个专栏主要还是为了利用费曼学习法来让自己更加深刻认识到数据结构的内容,在编程中还是会有很多我不清楚的地方,要走的路还有很多

二.认识双链表

双链表与链表的结构很相像,唯一不同的是,双链表里面有两个指针域,一个指向下一个节点,一个指向前一个节点,这就是他们之间最大的区别,在定义上,还是利用结构体

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

typedef struct Node{
    int Data;
    struct Node* next;    //指向下一个节点
    struct Node* prev;    //指向上一个节点
}Node;

三.双链表的创建

双链表的建立和链表也基本一致,依旧是利用malloc来动态申请内存

Node* Node_Create(int value){
    Node* New_Node = (Node*)malloc(sizeof(Node));
    if(New_Node == NULL){
        printf("内存申请失败\r\n");
        return NULL;
    }
    New_Node->Data = value;
    New_Node->next = NULL;
    New_Node->prev = NULL;
    return New_Node;
}

创造的双链表,两个指针域都指向空

四.双链表的头插法

在这里的时候我是受到了一个人的启发,就是通过设置接口来选择函数里面的参数(作者并不是计算机专业的,所以对这个不是特别了解)我将从头插法到尾插法来依次操作

第一次我的思路

通过头插法,将后插入的节点插在旧节点的前面,一次插入一个节点

//头插法
//对head进行一次解引用,则表示头节点本身的地址;对head进行二次解引用则是这个结构体本身
Node* Node_head(Node** head, int value) {
	Node* New_Node = Node_Create(value);
	if (New_Node == NULL) {
		printf("插入失败\r\n");
		return NULL;
	}

	New_Node->next = *head;	//插入新节点在之前头节点之前
	New_Node->prev = NULL;	//插入新节点的前驱指针指向空

	if (*head != NULL) {	        //头节点存在
		(*head)->prev = New_Node;	//原来头节点的前驱节点指向新节点
	}

	*head = New_Node;	//更新头指针

	return *head;
}

但是可以注意到,我这个写法其实不是很规范。这其实是属于融合了两种写法,显得有点不伦不类了,一般而言,这样的函数接口一般是有两种格式。

第一种:通过返回Node*类型,在函数里面的参数采用正常的指针,然后将变化的头指针给返回出去

第二种:通过返回void类型,直接在函数里面的参数里使用二级指针,然后在函数里面改变这个头指针,返回值就不会返回任何东西

所以按照这个思路,我就开始了第二次写法

第二次我的思路

方法一:

//插入一个节点
//头插法2.1
//直接返回节点地址,参数不需要为二级指针
Node* Node_head_1(Node* head, int value) {
	Node* New_Node = Node_Create(value);
	if (New_Node == NULL) {
		printf("插入失败\r\n");
		return NULL;
	}
	New_Node->next = head;
	New_Node->prev = NULL;

	if(head != NULL) {
		head->prev = New_Node;
	}
	head = New_Node;
	return head;
}

方法二:

//头插法2.2
//返回值为void类型,在函数里面直接改变头指针,没有返回值
void Node_head_2(Node** head, int value) {
	Node* New_Node = Node_Create(value);
	if (New_Node == NULL) {
		printf("插入失败\r\n");
		return;
	}

	New_Node->next = *head;
	New_Node->prev = NULL;

	if(*head != NULL) {
		(*head)->prev = New_Node;
	}
	*head = New_Node;
	return;
}

再此之后,我开始想到一次插入多个节点,也是根据这两种方法

第三次我的思路

方法一:

//插入多个节点
//头插法3.1
//返回头指针
Node* Node_head_2_1(Node* head, int n, int value[]) {
	int index = n-1;
	for (int j = 0; j <= index; j++) {
		Node* New_Node = Node_Create(value[j]);
		if (New_Node == NULL)
		{
			printf("插入失败\r\n");
			return NULL;
		}
		New_Node->next = head;
		New_Node->prev = NULL;

		if (head != NULL) {
			head->prev = New_Node;
		}
		head = New_Node;
	}
	return head;
}

方法二:

void Node_head_2_2(Node** head, int n, int value[]) {
	int index = n - 1;
	for (int j = 0; j <= index; j++) {
		Node* New_Node = Node_Create(value[j]);
		if (New_Node == NULL) {
			printf("插入失败\r\n");
			return;
		}
		New_Node->next = *head;
		New_Node->prev = NULL;

		if (*head != NULL) {
			(*head)->prev = New_Node;
		}
		*head = New_Node;
	}
	return;
}

当然,如果你觉得只要有一次插入失败就全盘推出不符合你的需求,也可以在那一部分改成


		if (New_Node == NULL) {
			printf("这是在第%d个插入失败的\r\n",j);
			continue;
		}

将return改成continue就可以了

五.双链表的尾插法

通过尾插法,将新节点放到旧节点的后面,感觉更符合人的一般规律,所以我也更喜欢使用尾插法

也是一样的,我将根据两次不一样的接口来写代码

第一次每次插入一个节点

第一次我的思路

方法一:

//尾插法1.1
void Node_tail_1(Node** head, int value) {
	Node* New_Node = Node_Create(value);
	if (New_Node == NULL) {
		printf("插入失败\r\n");
		return;
	}
	if (*head == NULL) {
		//链表为空
		*head = New_Node;
		return;
	}
	//链表不为空
	Node* tail = *head;
	while (tail->next != NULL) {
		tail = tail->next;
	}
	tail->next = New_Node;
	New_Node->prev = tail;
	//tail = New_Node;由于tail是一个局部变量,在这里不需要更新尾指针
	return;
}

方法二:

//尾插法1.2
Node* Node_tail_2(Node* head, int value) {
	Node* New_Node = Node_Create(value);
	if (New_Node == NULL) {
		printf("插入失败\r\n");
		return NULL;
	}
	if (head == NULL) {
		//链表为空
		head = New_Node;
		return head;
	}
	//链表不为空
	Node* tail = head;
	while (tail->next != NULL) {
		tail = tail->next;
	}
	tail->next = New_Node;
	New_Node->prev = tail;
	//tail = New_Node;
	return head;
}

第二次我的想法也自然而然转到了插入多个节点上

第二次我的思路

方法一:

//尾插法2.1
//一次插入多个节点
//但是这个复杂度达到了O(n²) ,每次都要从头找尾
void Node_tail_2_1(Node** head, int n, int value[]) {
	for (int j = 0; j < n; j++) {
		Node* New_Node = Node_Create(value[j]);
		if (New_Node == NULL) {
			printf("插入失败\r\n");
			return;
		}
		if (*head == NULL) {
			//链表为空
			*head = New_Node;
			continue;	//进入下一轮
		}
		//链表不为空
		Node* tail = *head;
		while(tail->next != NULL) {
			tail = tail->next;
		}
		tail->next = New_Node;
		New_Node->prev = tail;
		//tail = New_Node;
	}
	return;
}

方法二:

//尾插法2.2
//一次插入多个节点
Node* Node_tail_2_2(Node* head, int n, int value[]) {
	for (int j = 0; j < n; j++) {
		Node* New_Node = Node_Create(value[j]);
		if (New_Node == NULL) {
			printf("插入失败\r\n");
			return NULL;
		}
		if (head == NULL) {
			//链表为空
			head = New_Node;
			continue;
		}
		//链表不为空
		Node* tail = head;
		while (tail->next != NULL) {
			tail = tail->next;
		}
		tail->next = New_Node;
		New_Node->prev = tail;
		//tail = New__Node;
	}
	return head;
}

但很明显,这两个代码的都会从头开始遍历一遍链表,复杂度加大,很明显会有很简单的方法

第三次我的思路

方法一:

//尾插法3.1
void Node_tail_3_1(Node** head, int n, const int value[])
{
	if (!head) return;          //防野指针
	if (value == NULL || n <= 0) return head;
	Node* tail = *head;         //缓存当前尾节点
	if (tail)                   //找到真尾
		while (tail->next)
			tail = tail->next;

	for (int j = 0; j < n; ++j)
	{
		Node* new_node = Node_Create(value[j]);
		if (!new_node)
		{
			printf("第 %d 个节点申请失败,已插 %d 个\n", j, j);
			continue;           //保留已插节点
		}

		new_node->next = NULL;
		new_node->prev = tail;

		if (tail)               //非空表
			tail->next = new_node;
		else                    //空表
			*head = new_node;

		tail = new_node;        //更新尾指针
	}
}

方法二:

//尾插法3.2
Node* Node_tail_3_2(Node* head, int n, int value[]) {
	if (value == NULL || n <= 0) return head;
	Node* tail = head;
	if (tail != NULL) {
		while (tail->next != NULL) {
			tail = tail->next;
		}
	}
	for (int j = 0; j < n; j++) {
		Node* New_Node = Node_Create(value[j]);
		if (New_Node == NULL) {
			printf("第%d个节点插入失败,已插入%d个\r\n", j, j);
			continue;
		}
		New_Node->prev = tail;
		New_Node->next = NULL;

		if (tail != NULL) {
			tail->next = New_Node;
		}
		else {
			head = New_Node;
		}
		tail = New_Node;
	}
	return head;
}

六.双链表的遍历

按照数组的规则,我习惯上将第一个字节命名为第0个

//遍历双链表
void Node_Printf(Node* head) {
	Node* L = head;
	int i = 0;
	while(L != NULL){
		printf("第%d个节点是%d\r\n", i, L->Data);
		i++;
		L = L->next;
	}
	return;
}

七.总结

主要还是一些很简单的内容,逻辑还是最重要的,然后其他的增删改查之类的操作,我打算放在下一讲

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值