单链表与双链表实现

目录

一、链表概念

二、单链表的实现 

        2.1 定义结点

        2.2 具体实现功能如下:

        2.3 单链表打印

        2.4单链表插入 

               2.4.1 尾插

                2.4.2头插

        2.5 单链表删除

                2.5.1 尾删

                2.5.2 头删

        2.6 单链表关于指定节点的增与删

                2.6.1在指定位置之前插入数据

                2.6.2 在指定位置之后插入数据

                2.6.3删除pos节点

                2.6.4 删除pos之后的节点

        2.7 销毁链表

三、双链表的实现

        3.1 双链表初始化

        3.2 功能实现

        3.3 头插与尾插

                3.3.1 尾插

                3.3.2 头插

        3.4 头删与尾删

                3.4.1 头删

                3.4.2 尾删

        3.5 指定位置插入、修改数据

                3.5.1 指定位置后插入数据

        3.6 查找数据

        3.6 打印链表

        3.7 销毁链表

        3.8 总结

四、顺序表和双向链表的优缺点分析 

五、代码展示

        5.1 单链表

                5.1.1slist.c

                5.1.2 slist.h

        5.2 双链表

                5.2.1 list.c

                5.2.2 list.h


一、链表概念

        链表是什么?链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

        简单理解一下便是:链表可以近似的看作成火车,用节点一个一个串起来,需要操作时运用节点进行操作。

        其特点如下:

        链表由一系列节点(链表中每一个元素称为节点)组成,节点在运行时动态生成(malloc),每个节点包括两个部分:

     一个是存储数据元素的数据域

     另一个是存储下一个节点地址的指针域

          链表可分为以下几类:

带头不带头
单向

双向

循环不循环

        一共为8种组合(2*2*2),可用下图表示:

 

         虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构: 单向不带头不循环链表(单链表)双向带头循环链表(双链表)。

        原因如下:

        1. ⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构。

        2. 带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了。

二、单链表的实现 

        我们在实现链表时其实现目的与顺序表类似,无外乎为:“增删查改”四大功能。

        2.1 定义结点

typedef int SLTDataType;//使用目的:方便更改数据类型
typedef struct SListNode
{
	struct SListNode* next;
	SLTDataType date;
}SLTNode;

        注意:在定义节点(写成此结点也无所谓)时不要写成:SLTNode* next;因为编译器把结构体读完时才能达成重写条件。

        2.2 具体实现功能如下:

void SLTPrint(SLTNode* phead);

//头部插入删除/尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);

        接下来会带领大家一个一个实现。

        2.3 单链表打印

void SLTPrint(SLTNode* phead)
{
	SLTNode* p = p;
	while (p)
	{
		printf("%d-> ", p->date);
	}
	printf("NULL\n");
}

        在对链表进行打印时,我们不能像打印顺序表那样那样用for循环进行遍历, 因为链表在内存中并不是像顺序表线性存放,而是靠节点进行联系。在此处我们是否需要assert断言吗?大家可以稍作思考。

        答案是不需要,原因如下:若链表为空,不会进入for循环,直接打印NULL。

        2.4单链表插入 

        单链表插入像顺序表一样分为:头插与尾插,接下来会一一实现。

               2.4.1 尾插
Node* SLTBuyNode(SLTDataType x)
{
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
	if (node == NULL)
	{
		return -1;
	}
	node->date = x;
	node->next = NULL;
	return node;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x)//此处可猜测一下为什么使用二级指针
{
	assert(*pphead);
	SLTNode* newnode = SLTBuyNode(x);//因创建新节点方法会被多次使用,于是便单独封装出来
	if (*pphead == NULL)
	{
		*pphead = pphead;
	}
	else
	{
		SLTNode* p = *pphead;
		while (p)
		{
			p = p->next;
		}
		p->next = newnode;
	}
}

        在进行尾插时要注意以下几点 :

        首先,要使用二级指针来接收。在进行测试时发现使用一级指针无法改变实参,要改变实参只能传地址,一级指针传地址要用二级指针来接收。

        其次,要考虑头节点为零的情况。在头节点为空时,新开辟的节点便是我们要插入的节点。

        最后,为了使以后方便我们便创建了一个方法,需要使用使直接调用即可。

                2.4.2头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(*pphead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

           头插的代码相较于尾插能简单一点,只需要将新开辟出的节点的指向改成头节点,将头节点改为新开辟的节点即可。

        2.5 单链表删除

        和插入一样,单链表的删除同样分为头删和尾删。

                2.5.1 尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* p = *pphead;
		SLTNode* n;
		while (p->next)
		{
			n = p;
			p = p->next;
		}
		free(p);
		p = NULL;
		n->next = NULL;
		//法二
		//SLTNode* p = *pphead;
		//while(p->next->next)
		//{
		//	p = p->next;
		//}
		//free(p->next);
		//p->next = NULL;
	}
}

        写尾删注意如下:

        1.考虑到只有一个头节点。即该头节点指向空,可直接对其释放。

        2.若不为空,可使用两种方法:双指针或创造出的指针向后多指向一次。

        3.别忘记释放和置空。

                2.5.2 头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

        以上便是头删实现无需考虑情况,因为该节点若为头节点那它所指向的即为NULL。只需定义一个指向下一位的节点即可。

        2.6 单链表关于指定节点的增与删

                2.6.1在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	SLTNode* p = *pphead;
	if (pos == *pphead)
	{
		//调用头插
		SLTPushFront(pphead, x);
	}
	else
	{
		while (p->next != pos)
		{
			p = p->next;
		}
		newnode->next = pos;
		p->next = newnode;
	}
}

        在经行插入数据时,我们要创造头节点以及分析是否要分类讨论。

                2.6.2 在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

        这种插入代码比较简单创造出节点后即可快速完成。 

                2.6.3删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	if (pos == *pphead)
	{
		//调用头删
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* p = *pphead;
		while (p->next != pos)
		{
			p = p->next;
		}
		p->next = pos->next;
		free(p->next);
		p->next = NULL;
	}
}

        代码与之前插入类似。 

                2.6.4 删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	SLTNode* p = pos->next;
	pos->next = p->next;
	free(p);
	p = NULL;
}

        2.7 销毁链表

void SListDesTroy(SLTNode** pphead)
{
	SLTNode* p = *pphead;
	SLTNode* n = *pphead;
	while (p)
	{
		n = p->next;
		free(p);
		p = n;
	}
	*pphead = NULL;
}

        好了,以上便是我们单链表的学习了,请大家休息片刻,我们随后开始双链表的学习。

三、双链表的实现

        接下来,我们来学习双链表。双链表与单链表有相同也有不同,大家可在学习中自行体会。(在该链表使用一级指针而不用二级指针后面会说明)

        3.1 双链表初始化

LTNode* LTInit(LTDataType x)
{
	LTNode* phead = LTBuyNode(x);
	return phead;
}
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		return -1;
	}
	node->date = x;
	node->next = node;
	node->prve = node;
	return node;
}

        这里的LTBuyNode与上文单链表功能类似都是为了便于后面的使用。单、双链表的不同已在上文呈现,这里不过多介绍。

        3.2 功能实现

LTNode* LTInit(LTDataType x);//初始化
void LTDestroy(LTNode* phead);//销毁
void LTPrint(LTNode* phead);//打印
void LTPushBack(LTNode* phead, LTDataType x);//尾插
void LTPopBack(LTNode* phead);//尾删

void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPopFront(LTNode* phead);//头删

void LTInsert(LTNode* pos, LTDataType x);//在pos位置之后插入数据
void LTErase(LTNode* pos);//删除pos位置数据
LTNode* LTFind(LTNode* phead, LTDataType x);//查找数据

        3.3 头插与尾插

                3.3.1 尾插
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = phead;
	newnode->prve = phead->prve;
	phead->prve->next = newnode;//不可颠倒
	phead->prve = newnode;
}

        注意:后俩行代码位置不可交换。

                3.3.2 头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = phead->next;
	newnode->prve = phead;
	phead->next->prve = newnode;
	phead->next = newnode;
}

        注意点还是一样即:后俩行代码位置不可交换。

        3.4 头删与尾删

                3.4.1 头删
void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;
	phead->next = del->prve;
	del->next->prve = phead;
	free(del);
	del = NULL;
}

        注意:断言时要加入后半句,否则会删掉哨兵位。

                3.4.2 尾删
void LTPopBack(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->prve;
	phead->prve = del->prve;
	del->prve->next = phead;
	free(del);
	del = NULL;
}

        注意点还是一样,断言要加入后半句。

        3.5 指定位置插入、修改数据

                3.5.1 指定位置后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = pos->next;
	newnode->prve = pos;
	pos->next->prve = newnode;
	pos->next = newnode;
}

        注意:最后两行代码顺序不可修改。

                3.5.2 删除pos位置数据

void LTErase(LTNode* pos)
{
	assert(pos);
	pos->next->prve = pos->prve;
	pos->prve->next = pos->next;
	free(pos);
	pos = NULL;
}

        3.6 查找数据

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* p = phead->next;
	while (p != phead)
	{
		if (p->date == x)
		{
			return p;
		}
		p = p->next;
	}
	return NULL;
}

        3.6 打印链表

void LTPrint(LTNode* phead)
{
	LTNode* p = phead->next;
	while (p!=phead)
	{
		printf("%d->", p->date);
		p = p->next;
	}
	printf("NULL\n");
}

        打印方法与单链表类似。

        3.7 销毁链表

void LTDestroy(LTNode* phead)
{
	LTNode* p = phead->next;
	LTNode* n = phead->next;
	while (p != phead)
	{
		n = p->next;
		free(p);
		p = n;
	}
	//此时p指向phead,phead还没销毁
	free(phead);
	phead = NULL;
}
//存在问题传入一级指针后实参不会修改为NULL,需要手动修改

        3.8 总结

        为什么使用一级指针而不用二级指针?

        1.传入数据之前,链表要进行初始化,为了保护哨兵位,使用一级指针即可,若使用二级指针不会改变哨兵位也可以使用,不过还是推荐使用一级指针。

        2. 保持接口一致性,减少记忆成本。此链表接口过多,如若一会一级指针,一会二级指针会造成记忆成本。

四、顺序表和双向链表的优缺点分析 

        

不同点顺序表链表(单链表)
存储空间上物理上⼀定连续逻辑上连续,但物理上不⼀定连续
随机访问⽀持O(1)不⽀持:O(N)
任意位置插⼊或者删除元素可能需要搬移元素,效率低O(N)只需修改指针指向
插入动态顺序表,空间不够时需要扩 容没有容量的概念
应用场景元素⾼效存储+频繁访问任意位置插⼊和删除频繁

五、代码展示

        5.1 单链表

                5.1.1slist.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"slist.h"
void SLTPrint(SLTNode* phead)
{
	SLTNode* p = p;
	while (p)
	{
		printf("%d-> ", p->date);
	}
	printf("NULL\n");
}
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
	if (node == NULL)
	{
		return -1;
	}
	node->date = x;
	node->next = NULL;
	return node;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x)//此处可猜测一下为什么使用二级指针
{
	assert(*pphead);
	SLTNode* newnode = SLTBuyNode(x);//因创建新节点方法会被多次使用,于是便单独封装出来
	if (*pphead == NULL)
	{
		*pphead = pphead;
	}
	else
	{
		SLTNode* p = *pphead;
		while (p)
		{
			p = p->next;
		}
		p->next = newnode;
	}
}
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(*pphead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* p = *pphead;
		SLTNode* n;
		while (p->next)
		{
			n = p;
			p = p->next;
		}
		free(p);
		p = NULL;
		n->next = NULL;
		//法二
		//SLTNode* p = *pphead;
		//while(p->next->next)
		//{
		//	p = p->next;
		//}
		//free(p->next);
		//p->next = NULL;
	}
}
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	SLTNode* p = *pphead;
	if (pos == *pphead)
	{
		//调用头插
		SLTPushFront(pphead, x);
	}
	else
	{
		while (p->next != pos)
		{
			p = p->next;
		}
		newnode->next = pos;
		p->next = newnode;
	}
}
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;//位置不可颠倒
	pos->next = newnode;
}
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	if (pos == *pphead)
	{
		//调用头删
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* p = *pphead;
		while (p->next != pos)
		{
			p = p->next;
		}
		p->next = pos->next;
		free(p->next);
		p->next = NULL;
	}
}
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	SLTNode* p = pos->next;
	pos->next = p->next;
	free(p);
	p = NULL;
}
void SListDesTroy(SLTNode** pphead)
{
	SLTNode* p = *pphead;
	SLTNode* n = *pphead;
	while (p)
	{
		n = p->next;
		free(p);
		p = n;
	}
	*pphead = NULL;
}
                5.1.2 slist.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;//使用目的:方便更改数据类型
typedef struct SListNode
{
	struct SListNode* next;
	SLTDataType date;
}SLTNode;


void SLTPrint(SLTNode* phead);

//头部插入删除/尾部插入删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);

        5.2 双链表

                5.2.1 list.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"list.h"
LTNode* LTInit(LTDataType x)
{
	LTNode* phead = LTBuyNode(x);
	return phead;
}
void LTDestroy(LTNode* phead)
{
	LTNode* p = phead->next;
	LTNode* n = phead->next;
	while (p != phead)
	{
		n = p->next;
		free(p);
		p = n;
	}
	//此时p指向phead,phead还没销毁
	free(phead);
	phead = NULL;
}
void LTPrint(LTNode* phead)
{
	LTNode* p = phead->next;
	while (p!=phead)
	{
		printf("%d->", p->date);
		p = p->next;
	}
	printf("NULL\n");
}
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		return -1;
	}
	node->date = x;
	node->next = node;
	node->prve = node;
	return node;
}
void LTPushBack(LTNode* phead, LTDataType x)//尾插
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = phead;
	newnode->prve = phead->prve;
	phead->prve->next = newnode;//不可颠倒
	phead->prve = newnode;
}
void LTPopBack(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->prve;
	phead->prve = del->prve;
	del->prve->next = phead;
	free(del);
	del = NULL;
}

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = phead->next;
	newnode->prve = phead;
	phead->next->prve = newnode;
	phead->next = newnode;
}
void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;
	phead->next = del->prve;
	del->next->prve = phead;
	free(del);
	del = NULL;
}
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	newnode->next = pos->next;
	newnode->prve = pos;
	pos->next->prve = newnode;
	pos->next = newnode;
}
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->next->prve = pos->prve;
	pos->prve->next = pos->next;
	free(pos);
	pos = NULL;
}
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* p = phead->next;
	while (p != phead)
	{
		if (p->date == x)
		{
			return p;
		}
		p = p->next;
	}
	return NULL;
}
                5.2.2 list.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prve;
	LTDataType date;
}LTNode;

LTNode* LTInit(LTDataType x);//初始化
void LTDestroy(LTNode* phead);//销毁
void LTPrint(LTNode* phead);//打印

void LTPushBack(LTNode* phead, LTDataType x);//尾插
void LTPopBack(LTNode* phead);//尾删

void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPopFront(LTNode* phead);//头删

void LTInsert(LTNode* pos, LTDataType x);//在pos位置之后插入数据
void LTErase(LTNode* pos);//删除pos位置数据
LTNode* LTFind(LTNode* phead, LTDataType x);//查找数据

        链表经典练习题:CSDN 

        完!

  • 57
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值