IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 数据结构:链表-C语言实现(有图详讲带注释,适合入门学习) -> 正文阅读

[数据结构与算法]数据结构:链表-C语言实现(有图详讲带注释,适合入门学习)

链表

一. 前言

在学了顺序表之后,我们发现顺序表有一定的缺陷。第一个缺陷,从头部和中间的插入删除,都要移动后面的数据,时间复杂度为O(N)。第二个缺陷,增容需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。第三个缺陷,增容一般是呈2倍的增长,这会造成一定的空间浪费。比如说当前顺序表数据有1024个,容量也恰好是1024,这时我们只想插入一个数据,但是扩容却要扩大到2048个,这样有1023个空间大小就浪费了。刚刚提到的这些问题,链表就能很好地解决。下面我们就来一起学习一下链表,看看链表是怎么去解决这些问题的,链表又存在什么缺陷?

二. 链表的定义

2.1 概念

  • 链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。链表的所有节点都是一个一个单独通过malloc向内存申请的,用的时候再申请。从下图我们可以看出,链表的每个节点都有一个next指针指向下一个节点的地址,从逻辑上每个节点都是链接起来的。从内存的地址上看,每一个节点地址之间的距离大小都是不一样的,所以物理结构上他们不在的空间是不连续的。

在这里插入图片描述

2.2 分类

  • 单向和双向
    在这里插入图片描述

  • 带头和不带头
    在这里插入图片描述

  • 循环和不循环
    在这里插入图片描述

  • 实际中链表的结构非常多样,以上情况组合起来就有8种链表结构。虽然有这么多的链表的结构,但是我们实际中最常用还是这两种结构:单向无头不循环链表双向带头循环链表,下面我们来学习这两种链表。

三. 单向无头不循环链表

3.1 概念和说明

  • 单向无头不循环链表是链表中结构最简单的。如下图所示,每一个节点有一个data和一个nextdata是用来存放数据的,next是一个指向下一个节点地址的指针,最后一个节点的next指向NULL。在实现链表上,一个创建了三个文件,分别是SList.hSList.cmain.c,下面内容我们先定义链表的结构体和实现各个函数接口的代码,最后再把三个文件的代码展示出来。
    在这里插入图片描述

3.2 定义链表结构体

// 重定义数据类型名
typedef int SLTDataType;

// 定义链表结构体
typedef struct SListNode
{
	// 定义一个指向下一个节点地址的指针
	struct SListNode* next;

	// 定义一个存放数据的变量
	SLTDataType data;
}SListNode;
  • 为什么要重定义数据类型名?因为链表储存的元素类型不单单是int型,后面要存储char型的或者其他类型的数据,需要把代码里面的int都改一遍,非常麻烦。如果我们重新定义了类型名,并且在代码里用重新定义好的名字,下次需要存储其他类型的数据,直接在重定义那里把int改成想存储的类型就好了。

3.3 函数接口

3.3.1 申请节点

// 申请一个节点
SListNode* BuySListNode(SLTDataType x)
{
	// 向内存申请一个节点
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));

	// 判断申请是否成功
	assert(newnode);

	// 对节点初始化以及赋值
	newnode->next = NULL;
	newnode->data = x;

	return newnode;
}

在这里插入图片描述

3.3.2 链表头插

在这里插入图片描述

// 头插
/*
*********************
* 为什么会用到二级指针 *
* 后面3.7会讲到      *
*********************
*/
void SListPushFront(SListNode** pplist, SLTDataType x)
{
	// 防止传进来的pplist是空指针
	assert(pplist);

	// 申请一个新节点
	SListNode* newnode = BuySListNode(x);

	// 判断链表是否为空
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		 方法一
		  申请一个指针指向当前的头节点
		//SListNode* next = *pplist;
		//*pplist = newnode;
		//newnode->next = next;

		// 方法二
		newnode->next = *pplist;
		*pplist = newnode;
	}
}

// 方法一和方法二的补充
// 从上面我们可以看到方法一多定义了一个指针,指向当前头节点的地址
//这样做的好处是,在接下来的两条代码的顺序你可以随意变换

// 你可以这样写
*pplist = newnode;
newnode->next = next;

// 也可以这样写
newnode->next = next;
*pplist = newnode;

// 如果你像方法二那样没有定义指针的话,你的代码只能写成上面这个顺序
// 要是你顺序写反的话,*pplist会直接放弃原来的头节点去指向newnode,而当newnode的next想去指向原来的头节点时,已经找不到地址了。
// 所以正确的顺序是上面那样,先让newnode的next先指向原来的头节点,后面*pplist才去指向newnode。

// 总结,方法一多定义一个变量更加省心,方法二相对来说要思考得细一点,也便于我们更好地去理解链表结构。
  • assert是一个断言函数,程序运行的时候,当括号里面的结果为假时,就会停止运行并且报错。报错显示的信息包括断言的内容和断言的位置,还有一个错误框,如下图所示。断言能够快速地帮我们定位程序的错误,在实际开发中可以减少很多不必要的麻烦,所以建议大家在写代码的时候也尽量在需要的时候加上断言。

  • 温馨提示在使用assert函数时,记得包含一下assert.h这个头文件。
    在这里插入图片描述

3.3.3 链表尾插

在这里插入图片描述

// 尾插
void SListPushBack(SListNode** pplist, SLTDataType x)
{
	assert(pplist);

	// 申请一个新节点
	SListNode* newnode = BuySListNode(x);

	// 分两种情况,链表为空和非空
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		// 定义一个指针,去遍历寻找尾节点
		SListNode* tail = *pplist;
		while (tail->next)
		{
			tail = tail->next;
		}
		// 插入节点
		tail->next = newnode;
	}
}

3.3.4 在pos节点之后插入

在这里插入图片描述

// 在pos之后插入一个节点
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
	assert(pos);

	// 申请一个新节点
	SListNode* newnode = BuySListNode(x);
	
	 这里也是有两个方法,跟之前头插的差不多
	 方法一
	 定义一个指针指向pos的next
	//SListNode* posNext = pos->next;
	//newnode->next = posNext;
	//pos->next = newnode;

	// 方法二
	newnode->next = pos->next;
	pos->next = newnode;
}

3.3.5 在pos节点之前插入

在这里插入图片描述

// 在pos之前插入一个节点
void SListInsertBefore(SListNode** pplist, SListNode* pos, SLTDataType x)
{
	assert(pplist);
	assert(pos);

	// 申请一个新节点
	SListNode* newnode = BuySListNode(x);

	// 判断pos是否为第一个节点
	if (*pplist == pos)
	{
		newnode->next = pos;  
		*pplist = newnode;
	}
	else
	{
		// 先找到pos之前的一个节点
		SListNode* prev = *pplist;

		while (prev->next != pos)
		{
			prev = prev->next;
		}
		
		// 插入新节点
		newnode->next = pos;  
		prev->next = newnode;
	}
}

3.3.6 链表头删

在这里插入图片描述

// 头删
void SListPopFront(SListNode** pplist)
{
	// 防止pplist指针为空
	assert(pplist);

	// 防止pplist指向的地址为空,即链表为空
	assert(*pplist);

	// 定义一个指针指向第一个节点的地址,后面释放空间需要用到
	SListNode* temp = *pplist;

	// 让*pplist直接指向它的下一个节点
	*pplist = (*pplist)->next;

	// 释放被删节点空间,并把temp指针置空
	free(temp);
	temp = NULL;
}

3.3.7 链表尾删

在这里插入图片描述

// 尾删
void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);

	// 分两种情况,链表只有一个节点,和有一个以上节点
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		// 找到尾节点之前的一个节点
		SListNode* tail = *pplist;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		SListNode* temp = tail->next;
		tail->next = NULL;
		free(temp);
		temp = NULL;
	}
}

3.3.8 删去pos节点

在这里插入图片描述

// 删去pos节点
void SListErase(SListNode** pplist, SListNode* pos)
{
	assert(pplist);
	assert(*pplist);

	// 分两种情况,pos为第一个节点和不是第一个节点
	if (*pplist == pos)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		// 找到pos之前的节点
		SListNode* posPrev = *pplist;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

3.3.9 链表查找

// 查找
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
	// 当链表为空,返回NULL
	if (plist == NULL)
	{
		return NULL;
	}
	
	// 链表不为空,遍历链表
	SListNode* find = plist;
	while (find)
	{
		// 判断是否为所找节点,是则返回节点地址
		if (find->data == x)
		{
			return find;
		}
		// 继续迭代
		find = find->next;
	}
	// 没有找到,返回NULL
	return NULL;
}

3.3.10 链表修改

// 修改
void SListModify(SListNode* pos, SLTDataType x)
{
	assert(pos);
	pos->data = x;
}

3.3.11销毁链表

// 销毁
void SListDestroy(SListNode** pplist)
{
	assert(pplist);
	SListNode* temp = NULL;
    
    // 头删,依次释放空间
	while (*pplist)
	{
		temp = *pplist;
		*pplist = (*pplist)->next;
		free(temp);
	}
	temp = NULL;
}
  • 不知道大家是否记得上次顺序表的销毁是一次性把整块给销毁的,而这次的链表则要一个一个单独释放。因为顺序表我们是向内存申请一整块连续的空间而链表这边是一个一个单独申请的,且他们一般情况下是不连续的,所以释放得单独释放。

3.4 SList.h文件代码

#pragma once  // 防止头文件被重复包含


// 包含头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>


// 重新定义数据类型名
typedef int SLTDataType;


// 定义链表结构体
typedef struct SListNode
{
	// 定义一个指向下一个节点地址的指针
	struct SListNode* next;

	// 定义一个存放数据的变量
	SLTDataType data;
}SListNode;


// 函数接口

// 打印
void SListPrint(SListNode* plist);

// 申请一个节点
SListNode* BuySListNode(SLTDataType x);

// 头插
void SListPushFront(SListNode** pplist, SLTDataType x);

// 尾插
void SListPushBack(SListNode** pplist, SLTDataType x);

// 在pos之前插入一个节点
void SListInsertBefore(SListNode** pplist, SListNode* pos, SLTDataType x);

// 在pos之后插入一个节点
void SListInsertAfter(SListNode* pos, SLTDataType x);

// 头删
void SListPopfront(SListNode** pplist);

// 尾删
void SListPopBack(SListNode** pplist);

// 删去pos节点
void SListErase(SListNode** pplist, SListNode* pos);

// 查找
SListNode* SListFind(SListNode* plist, SLTDataType x);

// 修改
void SListModify(SListNode* pos, SLTDataType x);

// 销毁
void SListDestroy(SListNode** pplist)

3.5SList.c文件代码

#define _CRT_SECURE_NO_WARNINGS  // 这句是我的VS2019用scanf报错才加的,大家可以不用理
#include"SList.h"

// 打印
void SListPrint(SListNode* plist)
{
	while (plist)
	{
		printf("%d", plist->data);
		printf("-->");
		plist = plist->next;
	}
	printf("NULL");
}

// 申请一个节点
SListNode* BuySListNode(SLTDataType x)
{
	// 向内存申请一个节点
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));

	// 判断申请是否成功
	assert(newnode);

	// 对节点初始化以及赋值
	newnode->next = NULL;
	newnode->data = x;

	return newnode;
}

// 头插
void SListPushFront(SListNode** pplist, SLTDataType x)
{
	// 防止传进来的pplist是空指针
	assert(pplist);

	// 申请一个新节点
	SListNode* newnode = BuySListNode(x);

	// 判断链表是否为空
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		 方法一
		  申请一个指针指向当前的头节点
		//SListNode* next = *pplist;
		//*pplist = newnode;
		//newnode->next = next;

		// 方法二
		newnode->next = *pplist;
		*pplist = newnode;
	}
}

// 尾插
void SListPushBack(SListNode** pplist, SLTDataType x)
{
	assert(pplist);

	// 申请一个新节点
	SListNode* newnode = BuySListNode(x);

	// 分两种情况,链表为空和非空
	if (*pplist == NULL)
	{
		*pplist = newnode;
	}
	else
	{
		// 定义一个指针,去遍历寻找尾节点
		SListNode* tail = *pplist;
		while (tail->next)
		{
			tail = tail->next;
		}
		// 插入节点
		tail->next = newnode;
	}
}

// 在pos之前插入一个节点
void SListInsertBefore(SListNode** pplist, SListNode* pos, SLTDataType x)
{
	assert(pplist);
	assert(pos);

	// 申请一个新节点
	SListNode* newnode = BuySListNode(x);

	// 判断pos是否为第一个节点
	if (*pplist == pos)
	{
		newnode->next = pos;  
		*pplist = newnode;
	}
	else
	{
		// 先找到pos之前的一个节点
		SListNode* prev = *pplist;

		while (prev->next != pos)
		{
			prev = prev->next;
		}
		
		// 插入新节点
		newnode->next = pos;  
		prev->next = newnode;
	}
}

// 在pos之后插入一个节点
void SListInsertAfter(SListNode* pos, SLTDataType x)
{
	assert(pos);

	// 申请一个新节点
	SListNode* newnode = BuySListNode(x);
	
	 这里也是有两个方法,跟之前头插的差不多
	 方法一
	 定义一个指针指向pos的next
	//SListNode* posNext = pos->next;
	//newnode->next = posNext;
	//pos->next = newnode;

	// 方法二
	newnode->next = pos->next;
	pos->next = newnode;
}

// 头删
void SListPopFront(SListNode** pplist)
{
	// 防止pplist指针为空
	assert(pplist);

	// 防止pplist指向的地址为空,即链表为空
	assert(*pplist);

	// 定义一个指针指向第一个节点的地址,后面释放空间需要用到
	SListNode* temp = *pplist;

	// 让*pplist直接指向它的下一个节点
	*pplist = (*pplist)->next;

	// 释放被删节点空间,并把temp指针置空
	free(temp);
	temp = NULL;
}

// 尾删
void SListPopBack(SListNode** pplist)
{
	assert(pplist);
	assert(*pplist);

	// 分两种情况,链表只有一个节点,和有一个以上节点
	if ((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		// 找到尾节点之前的一个节点
		SListNode* tail = *pplist;
		while (tail->next->next)
		{
			tail = tail->next;
		}
		SListNode* temp = tail->next;
		tail->next = NULL;
		free(temp);
		temp = NULL;
	}
}

// 删去pos节点
void SListErase(SListNode** pplist, SListNode* pos)
{
	assert(pplist);
	assert(*pplist);

	// 分两种情况,pos为第一个节点和不是第一个节点
	if (*pplist == pos)
	{
		*pplist = pos->next;
		free(pos);
	}
	else
	{
		// 找到pos之前的节点
		SListNode* posPrev = *pplist;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
// 查找
SListNode* SListFind(SListNode* plist, SLTDataType x)
{
	// 当链表为空,返回NULL
	if (plist == NULL)
	{
		return NULL;
	}
	
	// 链表不为空,遍历链表
	SListNode* find = plist;
	while (find)
	{
		// 判断是否为所找节点,是则返回节点地址
		if (find->data == x)
		{
			return find;
		}
		// 继续迭代
		find = find->next;
	}
	// 没有找到,返回NULL
	return NULL;
}

// 修改
void SListModify(SListNode* pos, SLTDataType x)
{
	assert(pos);
	pos->data = x;
}

// 销毁
void SListDestroy(SListNode** pplist)
{
	assert(pplist);
	SListNode* temp = NULL;

	// 头删,依次释放空间
	while (*pplist)
	{
		temp = *pplist;
		*pplist = (*pplist)->next;
		free(temp);
	}
	temp = NULL;
}

3.6 main.c文件代码

#define _CRT_SECURE_NO_WARNINGS  //这句是我的VS2019用scanf报错才加的,大家可以不用理
#include"SList.h"


// 测试插入接口
void test1()
{
	SListNode* plist = NULL;
	SListPushBack(&plist, 4);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 1);
	SListPushBack(&plist, 5);
	SListPrint(plist);
	SListDestroy(&plist);
}

// 测试删除接口
void test2()
{
	SListNode* plist = NULL;
	SListPushBack(&plist, 4);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 1);
	SListPushBack(&plist, 5);
	//SListPopBack(&plist);
	//SListPopBack(&plist);
	//SListPopBack(&plist);
	//SListPopBack(&plist);
	//SListPopBack(&plist);
	//SListPopBack(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPopFront(&plist);
	SListPrint(plist);
	SListDestroy(&plist);
}

// 测试跟pos有关的接口
void test3()
{
	SListNode* plist = NULL;
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 1);
	SListNode* pos = SListFind(plist, 1);
	//SListInsertAfter(pos, 100);
	//SListInsertBefore(&plist, pos, 100);
	//SListErase(&plist, pos);
	SListModify(pos, 100000000);
	SListPrint(plist);
	SListDestroy(&plist);
}

int main()
{
	//test1();
	//test2();
	test3();
	return 0;
}

3.7 为什么要传二级指针

  • 在学习函数的时候,我们知道函数传参有两种,一种是传值,另外一种是传址。传值就是传变量的值过去,向下面的左边部分,我们实际上只是把10传过去给a,传20b。当我们在函数里把ab里面的值交换了,肯定不会影响到外面的xy了。而传址是变量的地址传过去,就像下图的右边,我们分别传x的地址给a指针,传y的地址给b指针。而当我们解引用时,*a等价于x变量。所以当我们在函数里面对*a*b修改,实际上就是直接对xy做修改,以实现交换两个变量的值。
    在这里插入图片描述

  • 那为什么这里我们需要传二级指针,plist本身不是一个指针吗?

  • 如下图所示,头插是需要改变plist的值的,一开始plist里面装的值是NULL,想要实现头插就是要改变plist里面的值,即改变plist指向的地址。plist是一个指针变量,就像上面说的例子一样,想要改变变量的值就要传变量的地址过去。所以这里传参需要传plist的地址过去,而plist是一个一级指针变量,它的地址需要一个二级指针去接收。下图我们&plist就是取出plist的地址,传过去给二级指针pplist,在头插函数里面,我们通过解引用*pplist就相当于plist,这样就实现了在函数改变plist的值。可以简单地理解为,函数传参,如果需要改变某个变量的值,就要传这个变量地址过去。接收那边,如果是普通变量的地址,比如说intchar这些类型变量的地址,接收那边就要拿对应int*char*这种类型的一级指针去接收。要是传过去的是像int*char*这些类型的一级指针的地址,接收的就要是对应的int**char**这样的二级指针去接收以此类推二级三级这些指针变量的地址,也是需要对应类型的三级四级指针去接收。有了以上的结论,再看我们这里,我们的plist的类型是SListNode*类型,也就是一个一级指针变量,接收的就要是一个SListNode**的变量去接收,也就一个二级指针变量。说了那么多,最重要的还是要记住这段话标粗的句子,像我们上面的查找函数接口,它不需要改变plist的值,所以就不用传地址,把plist的值传过去就ok了。
    在这里插入图片描述

四. 双向带头循环链表

4.1 概念和说明

双向带头循环链表是链表里面比较复杂的结构了,双向说的是它一个节点有两个指针。如图所示,一个是prev另外一个是next。一般情况下prev指向的是前一个节点的地址,next指向的是后面节点的地址。特殊情况,我们本次讨论的链表结构就是,第一个节点的prev指向的是最后一个节点,最后的一个节点的next指向第一个节点,这样就实现来循环。在下图的监视窗口我们也能看出,红色箭头指向的内存地址已经开始循环了,如果一直展开,也是这几个节点再循环。带头的意思是有一个头节点,像图中的head节点,它不会用来存储数据,这个节点也叫哨兵位。在实现链表上,一个创建了三个文件,分别是List.hList.cmain.c,下面内容我们先定义链表的结构体和实现各个函数接口的代码,最后再把三个文件的代码展示出来。大家也不要被名字给唬住了,这个结构虽然复杂了点,但是实现起来比上面的链表简单多了,下面我们一起学习一下吧。
在这里插入图片描述
在这里插入图片描述

4.2 定义链表结构体

// 重定义数据类型名
typedef int LTDataType;

// 定义结构体
typedef struct ListNode
{
	struct ListNode* prev;
	struct ListNode* next;
	LTDataType data;
}ListNode;

4.3 函数接口

4.3.1 初始化

// 初始化
ListNode* ListInit()
{
	// 向内存申请一个节点
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	assert(head);

	// 让两个指针都指向自己
	head->prev = head;
	head->next = head;
	return head;
}

4.3.2 打印链表

// 打印
void ListPrint(ListNode* head)
{
	ListNode* cur = head->next;

	// 遍历打印链表,当cur等于head已经循环了,所以这里时循环的出口
	while (cur != head)
	{
		printf("%d --> ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

4.3.3 申请节点

// 申请节点
ListNode* BuyListNode(LTDataType x)
{
	// 向内存申请一个节点
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));

	// 对新节点初始化和赋值
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	//返回新节点
	return newnode;
}

4.3.4 链表头插

在这里插入图片描述

// 头插
void ListPushFront(ListNode* head, LTDataType x)
{
	assert(head);

	// 申请新节点
	ListNode* newnode = BuyListNode(x);

	 方法1
	//ListNode* headNext = head->next;
	//newnode->next = headNext;
	//headNext->prev = newnode;
	//newnode->prev = head;
	//head->next = newnode;


	// 方法二
	newnode->next = head->next;
	head->next->prev = newnode;
	newnode->prev = head;
	head->next = newnode;
}

// 关于方法一和方法二
/*
跟之前的单单向无头不循环链表一样,方法一多定义了一个地址变量,存放插入过程中可能会被丢失的节点地址。在这个头插的操作过程中,由于控制不好操作的顺序可能会丢失head->next这个节点,所以方法一中多定义一个地址指针去保存它比较省心一些。方法二也不是没有好处,使用方法二会让我们对这个操作的过程更加理解,掌控也会更好。
*/

4.3.5 链表尾插

在这里插入图片描述

// 尾插
void ListPushBack(ListNode* head, LTDataType x)
{
	assert(head);
	ListNode* newnode = BuyListNode(x);

	 方法一
	//ListNode* tail = head->prev;
	//newnode->prev = tail;
	//tail->next = newnode;
	//newnode->next = head;
	//head->prev = newnode;

	// 方法二
	newnode->prev = head->prev;
	head->prev->next = newnode;
	newnode->next = head;
	head->prev = newnode;
}

4.3.6 在pos之前插入

在这里插入图片描述

// 在pos之前插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newnode = BuyListNode(x);

	 方法一
	//ListNode* posPrev = pos->prev;
	//newnode->prev = posPrev;
	//posPrev->next = newnode;
	//newnode->next = pos;
	//pos->next = newnode;
	
	// 方法二
	newnode->prev = pos->prev;
	pos->prev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
}

4.3.7 链表头删

在这里插入图片描述

// 头删
void ListPopFront(ListNode* head)
{
	assert(head);

	// 判断空表,这很重要!!!
	assert(head->next != head);

	// 定义一个指针指向被删节点的地址,方便后面释放它的空间
	ListNode* temp = head->next;


	// 方法一
	 定义指针指向被删节点后面一个节点
	//ListNode* tempNext = temp->next;

	 删除节点
	//head->next = tempNext;
	//tempNext->prev = head;

	 释放节点和置空指针
	//free(temp);
	//temp = NULL;
	//tempNext = NULL;



	// 方法二
	// 删除节点
	head->next = head->next->next;
	head->next->prev = head;

	// 释放节点
	free(temp);
	temp = NULL;
}

4.3.8 链表尾删

在这里插入图片描述

// 未删
void ListPopBack(ListNode* head)
{
	assert(head);

	// 判断空表
	assert(head->next != head);

	// 保存被删节点的地址
	ListNode* temp = head->prev;

	 方法一
	 记录被删节点的前一个节点地址
	//ListNode* tempPrev = temp->prev;

	 删除节点
	//head->prev = tempPrev;
	//tempPrev->next = head;

	 释放被删节点,置空指针
	//free(temp);
	//temp = NULL;
	//tempPrev = NULL;


	// 方法二
	// 删除节点
	head->prev = head->prev->prev;
	head->prev->next = head;

	// 释放被删节点,置空指针
	free(temp);
	temp = NULL;
}

4.3.9 删除pos节点

在这里插入图片描述

// 删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);

	// 判断空表
	assert(pos->next != pos);
	
	 方法一
	 保存pos前后两个节点
	//ListNode* posPrev = pos->prev;
	//ListNode* posNext = pos->next;

	 删除pos节点
	//posPrev->next = posNext;
	//posNext->prev = posPrev;

	 释放pos节点
	//free(pos);
	//pos = NULL;
	//posPrev = NULL;
	//posNext = NULL;


	// 方法二
	// 删除pos节点
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;

	// 释放pos
	free(pos);
	pos = NULL;
}

4.3.10 查找

// 查找
ListNode* ListFind(ListNode* head, LTDataType x)
{
	assert(head);

	// 从head的下一个节点开始遍历
	ListNode* find = head->next;
	while (find != head)
	{
		// 如果找到了,返回当前节点
		if (find->data == x)
		{
			return find;
		}
		find = find->next;
	}
	// 走到这里,找不到,返回NULL
	return NULL;
}

4.3.11 修改

// 修改
void ListModify(ListNode* pos, LTDataType x)
{
	assert(pos);
	pos->data = x;
}

4.3.12 链表的销毁

// 销毁链表
void ListDstroy(ListNode* head)
{
	assert(head);

	// 头删法,依次遍历链表,删除和释放各个节点,直到只剩下head节点
	ListNode* temp = NULL;
	while (head->next != head)
	{
		// 记录要删除的节点地址
		temp = head->next;

		// 从链表中删除节点
		head->next = head->next->next;
		head->next->prev = head;

		// 释放删除的节点
		free(temp);
	}

	// 释放头节点和置空指针
	free(head);
	head = NULL;
	temp = NULL;
}

4.4 List.h文件代码

#pragma once  // 防止头文件被重复包含


// 包含头文件
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>


// 重定义数据类型名
typedef int LTDataType;


// 定义结构体
typedef struct ListNode
{
	struct ListNode* prev;
	struct ListNode* next;
	LTDataType data;
}ListNode;


// 函数声明

// 初始化
ListNode* ListInit();

// 打印
void ListPrint(ListNode* head);

// 申请节点
ListNode* BuyListNode(LTDataType x);

// 头插
void ListPushFront(ListNode* head, LTDataType x);

// 尾插
void ListPushBack(ListNode* head, LTDataType x);

// 在pos之前插入
void ListInsert(ListNode* pos, LTDataType x);

// 头删
void ListPopFront(ListNode* head);

// 未删
void ListPopBack(ListNode* head);

// 删除pos位置的节点
void ListErase(ListNode* pos);

// 查找
ListNode* ListFind(ListNode* head, LTDataType x);

// 修改
void ListModify(ListNode* pos, LTDataType x);

// 销毁链表
void ListDstroy(ListNode* head);

4.5 List.c文件代码

#define _CRT_SECURE_NO_WARNINGS  //这句是我的VS2019用scanf报错才加的,大家可以不用理 
#include"List.h"

// 初始化
ListNode* ListInit()
{
	// 向内存申请一个节点
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	assert(head);

	// 让两个指针都指向自己
	head->prev = head;
	head->next = head;
	return head;
}

// 打印
void ListPrint(ListNode* head)
{
	ListNode* cur = head->next;

	// 遍历打印链表,当cur等于head已经循环了,所以这里时循环的出口
	while (cur != head)
	{
		printf("%d --> ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

// 申请节点
ListNode* BuyListNode(LTDataType x)
{
	// 向内存申请一个节点
	ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));

	// 对新节点初始化和赋值
	newnode->data = x;
	newnode->next = NULL;
	newnode->prev = NULL;

	//返回新节点
	return newnode;
}

// 头插
void ListPushFront(ListNode* head, LTDataType x)
{
	assert(head);

	// 申请新节点
	ListNode* newnode = BuyListNode(x);

	 方法1
	//ListNode* headNext = head->next;
	//newnode->next = headNext;
	//headNext->prev = newnode;
	//newnode->prev = head;
	//head->next = newnode;


	// 方法二
	newnode->next = head->next;
	head->next->prev = newnode;
	newnode->prev = head;
	head->next = newnode;
}

// 尾插
void ListPushBack(ListNode* head, LTDataType x)
{
	assert(head);
	ListNode* newnode = BuyListNode(x);

	 方法一
	//ListNode* tail = head->prev;
	//newnode->prev = tail;
	//tail->next = newnode;
	//newnode->next = head;
	//head->prev = newnode;

	// 方法二
	newnode->prev = head->prev;
	head->prev->next = newnode;
	newnode->next = head;
	head->prev = newnode;
}

// 在pos之前插入
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);
	ListNode* newnode = BuyListNode(x);

	 方法一
	//ListNode* posPrev = pos->prev;
	//newnode->prev = posPrev;
	//posPrev->next = newnode;
	//newnode->next = pos;
	//pos->next = newnode;
	
	// 方法二
	newnode->prev = pos->prev;
	pos->prev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
}

// 头删
void ListPopFront(ListNode* head)
{
	assert(head);

	// 判断空表,这很重要!!!
	assert(head->next != head);

	// 定义一个指针指向被删节点的地址,方便后面释放它的空间
	ListNode* temp = head->next;


	// 方法一
	 定义指针指向被删节点后面一个节点
	//ListNode* tempNext = temp->next;

	 删除节点
	//head->next = tempNext;
	//tempNext->prev = head;

	 释放节点和置空指针
	//free(temp);
	//temp = NULL;
	//tempNext = NULL;



	// 方法二
	// 删除节点
	head->next = head->next->next;
	head->next->prev = head;

	// 释放节点
	free(temp);
	temp = NULL;
}

// 未删
void ListPopBack(ListNode* head)
{
	assert(head);

	// 判断空表
	assert(head->next != head);

	// 保存被删节点的地址
	ListNode* temp = head->prev;

	 方法一
	 记录被删节点的前一个节点地址
	//ListNode* tempPrev = temp->prev;

	 删除节点
	//head->prev = tempPrev;
	//tempPrev->next = head;

	 释放被删节点,置空指针
	//free(temp);
	//temp = NULL;
	//tempPrev = NULL;


	// 方法二
	// 删除节点
	head->prev = head->prev->prev;
	head->prev->next = head;

	// 释放被删节点,置空指针
	free(temp);
	temp = NULL;
}

// 删除pos位置的节点
void ListErase(ListNode* pos)
{
	assert(pos);

	// 判断空表
	assert(pos->next != pos);
	
	 方法一
	 保存pos前后两个节点
	//ListNode* posPrev = pos->prev;
	//ListNode* posNext = pos->next;

	 删除pos节点
	//posPrev->next = posNext;
	//posNext->prev = posPrev;

	 释放pos节点
	//free(pos);
	//pos = NULL;
	//posPrev = NULL;
	//posNext = NULL;


	// 方法二
	// 删除pos节点
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;

	// 释放pos
	free(pos);
	pos = NULL;
}

// 查找
ListNode* ListFind(ListNode* head, LTDataType x)
{
	assert(head);

	// 从head的下一个节点开始遍历
	ListNode* find = head->next;
	while (find != head)
	{
		// 如果找到了,返回当前节点
		if (find->data == x)
		{
			return find;
		}
		find = find->next;
	}
	// 走到这里,找不到,返回NULL
	return NULL;
}

// 修改
void ListModify(ListNode* pos, LTDataType x)
{
	assert(pos);
	pos->data = x;
}

// 销毁链表
void ListDstroy(ListNode* head)
{
	assert(head);

	// 头删法,依次遍历链表,删除和释放各个节点,直到只剩下head节点
	ListNode* temp = NULL;
	while (head->next != head)
	{
		// 记录要删除的节点地址
		temp = head->next;

		// 从链表中删除节点
		head->next = head->next->next;
		head->next->prev = head;

		// 释放删除的节点
		free(temp);
	}

	// 释放头节点和置空指针
	free(head);
	head = NULL;
	temp = NULL;
}

4.6 main.c文件代码

#define _CRT_SECURE_NO_WARNINGS  //这句是我的VS2019用scanf报错才加的,大家可以不用理
#include"List.h"

// 测试插入接口
void Test1()
{
	ListNode* head = ListInit();
	ListPushFront(head, 3);
	ListPushFront(head, 2);
	ListPushFront(head, 1);
	ListPushBack(head, 4);
	ListPushBack(head, 5);
	ListPushBack(head, 6);
	ListPrint(head);
	ListDstroy(head);
	printf("over\n");
}

// 测试插入接口
void Test2()
{
	ListNode* head = ListInit();
	ListPushBack(head, 4);
	ListPushBack(head, 5);
	ListPushBack(head, 6);
	//ListPopFront(head);
	//ListPopFront(head);
	//ListPopFront(head);
	//ListPopFront(head);
	//ListPopFront(head);
	ListPopBack(head);
	ListPopBack(head);
	ListPopBack(head);
	//ListPopBack(head);
	ListPrint(head);
	ListDstroy(head);
	printf("over\n");
}

// 测试与pos相关接口
void Test3()
{
	ListNode* head = ListInit();
	ListPushBack(head, 1);
	ListPushBack(head, 2);
	ListPushBack(head, 3);
	ListNode* pos = ListFind(head, 2);
	//ListInsert(pos, 200);
	ListErase(pos);
	//ListModify(pos, 200000000);
	ListPrint(head);
	ListDstroy(head);
	printf("over\n");
}

int main()
{
	//Test1();
	//Test2();
	Test3();
	return 0;
}

五. 顺序表和链表的优缺点

5.1 顺序表

5.1.1 顺序表的优点

  1. 支持随机访问(用下标访问)。
  2. CPU高速缓存命中率更高(下面5.3有解释)。

5.1.2 顺序表的缺点

  1. 头部中部插入删除时间效率低,O(N)。
  2. 扩容有一定程度的性能消耗,扩容一般是按倍数去阔,用不完会造成一定的空间浪费。

5.2 链表

5.2.1 链表的优点

  1. 任意位置插入删除效率高,O(1)。
  2. 按需申请和释放空间,不会造成空间浪费。

5.2.2 链表的缺点

  1. 不支持随机访问,意味这一些排序和二分查找在链式结构上不适用。
  2. 每个节点除了要存数据还需要指针去存其他节点的地址。
  3. CPU高速缓存命中率更低。

5.3 CPU高速缓存命中率

  • 目前主流的计算机存储系统大致分为主存和外存,外存就是U盘,磁盘,硬盘这些,主存就是我们经常说的内存了。买电脑的时候我们会发现,目前主流的电脑,硬盘动不动就是以TB为单位,而内存还是GB,一般是4GB,8GB,或者16GB。为什么他们相差那么大呢,因为内存比较贵,当然好处就是速度也比他们快很多。虽然目前计算机内存的速度速度已经很不错了,但是CPU的发展比内存更快,速度远远超过了内存。为了电脑的整体性能,人们又在CPU引入了跟其速度相当的高速缓存,如下图的Cache,它们的容量非常小,一般是以MB为单位,而且是个位数或者十位数的。高速缓存一般又分为一级缓存,二级缓存,和三级缓存,一级缓存速度最快,也最接近CPU。一般电脑在运行时,电脑会从内存中调用一部分到高速缓存中,CPU需要的数据首先会到高速缓存中找,找到不到才会到内存中。在高速缓存中找到就叫做命中,那为什么上面我们说顺序表的命中率呢?因为顺序表本质上是数组,是内存中一块连续的空间,而链表它在内存中不是一块连续的空间。顺序表里面的数据命中了就很有可能连续被命中,链表一个数据被命中了,下一个可能不在高速缓存就不会命中,所以命中率就低了。
    在这里插入图片描述

六. 总结

从上面我们可以看出,顺序表和链表各有优缺点,一般顺序表的优点就链表的缺点,反之顺序表的缺点就是链表的优点。这两种结构是相互弥补的,在实际开发中如果需要频繁插入和删除大量数据的话链表会更优一些,如果需要按照下标访问又是顺序表更加合适。所以说不能说他们谁最好,要看自己的需求。顺序表和链表是数据结构的开端,也是非常重要的知识,学好这两种结构,有助于我们更好地后面学习和理解其他结构。这两部分包括后续的章节对C语言的指针,函数,结构体和动态内存规划这几个方面的知识要求比较高,特别是指针。所以建议大家要把这几个方面的知识掌握扎实,这样才能更好地学习数据结构。最后,本文是我学完这章内容后的个人总结,要是文章里有什么错误还望各位大神指正。或者对我的文章排版和其他方面有什么建议,也可以在评论区告诉我。如果我的文章对你的学习有帮助,或者觉得写得不错的话记得分享给你的朋友,非常感谢。

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-11-18 11:25:30  更:2021-11-18 11:25:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 11:52:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码