数据结构与算法(C语言版)P3.1---链表(无头单向非循环链表)

1、链表的概念及结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构。数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

在这里插入图片描述

注意:

​ 1、从上图可看出:链式结构在逻辑上是连续的,但是在物理上不一定连续。

​ 2、现实中的结点一般都是从堆上申请出来的。

​ 3、从堆上申请空间,是按照一定策略来分配的,再次申请的空间可能连续,不可能不连续。

链表中的结点也是动态内存分配空间,按需分配。

假设在32位系统上,结点中值域为int类型,则一个结点的大小位8字节,则可能有如下链表:

数据结构与算法(C语言版)P3.1---链表(无头单向非循环链表)_第1张图片

链表结构中的结点由两部分组成:

  • 数据域:存储元素数值数据。
  • 指针域:存储直接后继节点的存储位置。

2、链表的分类

实际中链表的结构非常多,以下情况组合起来就有8中链表结构:

数据结构与算法(C语言版)P3.1---链表(无头单向非循环链表)_第2张图片

结点只有一个指针域的链表,称为单链表或线性表。

结点有两个指针域的链表,称为双链表。

首尾相接的链表称为循环链表。

头指针、头结点和首元结点:

数据结构与算法(C语言版)P3.1---链表(无头单向非循环链表)_第3张图片

头指针:是指向链表中第一个结点的指针。

首元结点:是指链表中存储第一个数据元素a1的结点。

头结点(哨兵位):是在链表的首元结点之前附设的一个结点,此节点不存储数据,只记录首元结点的地址。

讨论一:在链表中设置头结点有什么好处?

1、便于首元节点的处理:

​ 首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行 特殊处理。

2、便于空表和非空表的统一处理:

​ 无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。

讨论二:头结点的数据域内装的是什么呢?

​ 头结点的数据域可以为空,也可以存放线性表长度等附加信息,但此结点不能计入链表长度值。

1、单向或者双向

数据结构与算法(C语言版)P3.1---链表(无头单向非循环链表)_第4张图片

2、带头或者不带头

数据结构与算法(C语言版)P3.1---链表(无头单向非循环链表)_第5张图片

3、循环和非循环

数据结构与算法(C语言版)P3.1---链表(无头单向非循环链表)_第6张图片

虽然有那么多的链表结构,但是我们实际中最常用的还是两种结构:

​ 1、无头单向非循环链表

数据结构与算法(C语言版)P3.1---链表(无头单向非循环链表)_第7张图片

​ 2、带头双向循环链表

1、无头单向非循环链表:结构简单,一般不会单独用来__存放数据__,实际中更多是作为__其它数据结构的子结构__,如:哈希桶,图的邻接表等等。另外这种结构在笔试中出现很多。

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

数据结构与算法(C语言版)P3.1---链表(无头单向非循环链表)_第8张图片
所以下面我们就实现以上两个最常用的链表。

3、无头单向非循环链表的实现

链表主要有以下几个接口功能:

  • 打印(SListPrint)
  • 销毁链表(SListDestory)
  • 扩容(BuyListNode)
  • 头插(SListPushFront)
  • 头删(SListPopFront)
  • 尾插(SListPushBack)
  • 尾删(SListPopBack)
  • 查找元素(SListFind)
  • 在pos位置之前插入(SListInsert)
  • 在pos位置之后去插入(SListErase)
  • 删除pos当前位置的数据(SeqListErase)
  • 删除pos位置之后的一个结点(SListEraseAfter)

和顺序表不同的是,无头单向非循环链表是不用初始化的。所以没有初始化接口。我们只需要在main函数中申请一个结点地址即可。如下:

int main()
{
	SLTNode* plist = NULL;
	return 0;
}

3.1、定义结构体

#pragma once;

#include 
#include 
#include 

typedef int SLDataType;

typedef struct SListNode
{
	SLDataType data;
	struct SListNode* Next;
}SLTNode;

data变量用于存储数据,叫做数据域。
Next指针变量用于存储下一个结点的地址,叫做指针域。

3.2、扩容

链表的好处之一就是存储一个数据就申请一块空间,所以在链表中进行任意擦汗如操作时,都需要先申请内存空间。

SLTNode* BuyListNode(SLDataType x)
{
	//创建新节点(就是需要尾插的那个结点)
	//注意这个newnode指针,它就是新节点的地址,后面将尾结点和新节点关联的时候,就需要用到此地址。
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));

	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->Next = NULL;
	return newnode;
}

3.3、头插

void SListPushFront(SLTNode** pphead, SLDataType x)
{
	SLTNode* newnode = BuyListNode(x);
	newnode->Next = *pphead;
	*pphead = newnode;
}

3.4、打印

这里打印只需要传一级指针即可。只需打印输出,无需其它操作,所以传一级指针即可。

void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d ", cur->data);
		cur = cur->Next;
	}
	printf("\n");
}

3.5、销毁

销毁链表的错误做法,不能直接操作:free(*pphead);
如果这样,只释放了头结点的地址,后面的结点的地址,都会找不到且无法释放。所以会造成内存泄漏。

正确的操作:一个结点一个结点的释放,并且在释放一个结点之前先把此结点的地址先保存来下来,以防找不到。

void SListDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->Next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

3.6、头删

头删的核心思想:先保存第二个结点的地址,然后释放头结点,最后再将头指针和第二个结点的地址链接起来。

void SListPopFront(SLTNode** pphead)
{
	assert(*pphead != NULL);
	
	//保存第二个结点
	SLTNode* next = (*pphead)->Next;
	//释放第一个结点
	free(*pphead);
	//重新链接结点
	*pphead = next;
}

3.7、尾插

尾插的核心思想:找到最后一个结点,然后和扩容的newnode新节点进行链接。

需要注意的是:由于我们实现的是无头链表,所以在进行结点的插入以及删除时,需要考虑,空链表,只有一个结点的特殊情况。

void SListPushBack(SLTNode** pphead, SLDataType x)
{
	SLTNode* newnode = BuyListNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
	//首先需要找到链表中的尾结点,尾结点有个特征:它的Next为NULL。
		SLTNode* tail = *pphead;
		while (tail->Next)
		{
			tail = tail->Next;
		}
		//关联尾节点和新节点
		tail->Next = newnode;
	}
}

3.8、尾删

尾删的核心思想:同时找到最后一个结点和倒数第二个结点。然后释放最后一个结点,并且将倒数第二个结点的Next指为NULL。

这里也需要考虑3种情况:没有结点,只有一个结点,有多个结点。

void SListPopBack(SLTNode** pphead)
{
	//先处理没有结点的情况,直接断言即可。
	assert(*pphead != NULL);

	//其次处理只有一个结点的情况
	if ((*pphead)->Next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//最后考虑的就是有多个结点的情况
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;

		while (tail->Next)
		{
			prev = tail;
			tail = tail->Next;
		}
		free(tail);
		tail = NULL;
		prev->Next = NULL;
	}
}

3.9、查询指定元素,并返回结点地址pos

SLTNode* SListFind(SLTNode* phead, SLDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (x == cur->data)
		{
			return cur;
		}
		else
		{
			cur = cur->Next;
		}
	}
	return NULL;
}

3.10、在pos位置之前插入

核心思想:找到pos结点前面的结点,然后依次链接起来即可。

特殊情况:需要考虑只有一个结点和多个结点的情况

void SListInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
	SLTNode* newnode = BuyListNode(x);
	//这个用于只有一个结点的情况
	if (*pphead == pos)
	{
		*pphead = newnode;
		newnode->Next = pos;
	}
	//这个用于多个结点前插入数据
	else
	{
		SLTNode* posPrev = *pphead;
		//找到pos的前一个位置
		while (posPrev->Next != pos)
		{
			posPrev = posPrev->Next;
		}
		//链接
		posPrev->Next = newnode;
		newnode->Next = pos;
	}
}

在pos之前插入的时间复杂度分析。这里分三种情况:

  • 直接是头插,那时间复杂度是O(1)
  • 需要插入的数据在中间
  • 需要插入的数据在最后一个结点前,所以是O(N)。

所以取悲观预期,综合来说时间复杂度是O(N)。

3.11、在pos位置之后插入

在pos位置之后插入比在pos位置之前插入简单。就直接依次链接即可。

void SListInsertAfter(SLTNode** pphead, SLTNode* pos,SLDataType x)
{
	SLTNode* newnode = BuyListNode(x);

	SLTNode* posNext = pos->Next;
	pos->Next = newnode;
	newnode->Next = posNext;
}

在pos之后插入的时间复杂度分析:
在pos位置之后插入,由于确定好了插入的位置,可以直接插入。所以时间复杂度为O(1)。

相比在pos位置之前插入,结点,更推荐使用pos位置之后插入结点。

3.12、删除pos当前位置结点

核心思想:找到pos前一个结点,将pos前一个结点和pos后一个结点链接,然后释放pos。

考虑情况:

  • 只有一个结点
  • 有多个结点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	//删除头结点
	if (*pphead == pos)
	{
		*pphead = pos->Next;
		free(pos);
	}
	//删除非头结点的其它结点。
	else
	{
		SLTNode* posPrev = *pphead;
		while (posPrev->Next != pos)
		{
			posPrev = posPrev->Next;
		}
		posPrev->Next = pos->Next;
		free(pos);
	}
}

删除pos位置当前的时间复杂度:O(N)。

3.13、删除pos位置之后的结点

很简单,直接链接。

void SListEraseAfter(SLTNode* pos)
{
	assert(pos->Next);
	SLTNode* next = pos->Next;
	pos->Next = next->Next;
	free(next);
}

在pos位置之后的时间复杂度:O(1)。

4、全代码展示

这里使用三个文件:

  • seqlist.h:用于结构体、各种函数接口的声明
  • seqlist.c:用于各种函数接口的定义。
  • test.c:用于创建链表,实现链表。

4.1、seqlist.h

#pragma once;

#include 
#include 
#include 

typedef int SLDataType;

typedef struct SListNode
{
	SLDataType data;
	struct SListNode* Next;
}SLTNode;

//扩容
SLTNode* BuyListNode(SLDataType x);

//头插
void SListPushFront(SLTNode** pphead, SLDataType x);

//打印
void SListPrint(SLTNode* phead);

//销毁
void SListDestroy(SLTNode** pphead);

//头删
void SListPopFront(SLTNode** pphead);

//尾插
void SListPushBack(SLTNode** pphead, SLDataType x);

//尾删
void SListPopBack(SLTNode** pphead);

//查询指定元素,返回结点地址
SLTNode* SListFind(SLTNode* phead,SLDataType x);

//在pos位置之前插入结点
void SListInsert(SLTNode** phead, SLTNode* pos, SLDataType x);

//在pos位置之后插入结点
void SListInsertAfter(SLTNode** pphead, SLTNode* pos,SLDataType x);

//删除当前pos位置结点
void SListErase(SLTNode** pphead, SLTNode* pos);

//删除pos位置之后的结点
void SListEraseAfter(SLTNode* pos);

4.2、seqlist.c

#include "list.h"

//扩容
SLTNode* BuyListNode(SLDataType x)
{
	//创建新节点(就是需要尾插的那个结点)
	//注意这个newnode指针,它就是新节点的地址,后面将尾结点和新节点关联的时候,就需要用到此地址。
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));

	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->Next = NULL;
	return newnode;
}

//头插
void SListPushFront(SLTNode** pphead, SLDataType x)
{
	SLTNode* newnode = BuyListNode(x);
	newnode->Next = *pphead;
	*pphead = newnode;
}

//打印
void SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		printf("%d ", cur->data);
		cur = cur->Next;
	}
	printf("\n");
}

//销毁
void SListDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->Next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

//头删
void SListPopFront(SLTNode** pphead)
{
	assert(*pphead != NULL);
	
	//保存第二个结点
	SLTNode* next = (*pphead)->Next;
	//释放第一个结点
	free(*pphead);
	//重新链接结点
	*pphead = next;
}

//尾插
void SListPushBack(SLTNode** pphead, SLDataType x)
{
	SLTNode* newnode = BuyListNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->Next)
		{
			tail = tail->Next;
		}
		tail->Next = newnode;
	}
}

//尾删
void SListPopBack(SLTNode** pphead)
{
	if ((*pphead)->Next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;

		while (tail->Next)
		{
			prev = tail;
			tail = tail->Next;
		}
		free(tail);
		tail = NULL;
		prev->Next = NULL;
	}
}

//查询指定元素,返回结点地址
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (x == cur->data)
		{
			return cur;
		}
		else
		{
			cur = cur->Next;
		}
	}
	return NULL;
}

//在pos位置之前插入结点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
	SLTNode* newnode = BuyListNode(x);
	
	if (*pphead == pos)
	{
		*pphead = newnode;
		newnode->Next = pos;
	}
	else
	{
		SLTNode* posPrev = *pphead;
		while (posPrev->Next != pos)
		{
			posPrev = posPrev->Next;
		}
		posPrev->Next = newnode;
		newnode->Next = pos;
	}
}

//在pos位置之后插入结点
void SListInsertAfter(SLTNode** pphead, SLTNode* pos,SLDataType x)
{
	SLTNode* newnode = BuyListNode(x);

	SLTNode* posNext = pos->Next;
	pos->Next = newnode;
	newnode->Next = posNext;
}

//删除当前pos位置结点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
	if (*pphead == pos)
	{
		*pphead = pos->Next;
		free(pos);
	}
	else
	{
		SLTNode* posPrev = *pphead;
		while (posPrev->Next != pos)
		{
			posPrev = posPrev->Next;
		}
		posPrev->Next = pos->Next;
		free(pos);
	}
}

//删除pos位置之后的结点
void SListEraseAfter(SLTNode* pos)
{
	assert(pos->Next);
	SLTNode* next = pos->Next;
	pos->Next = next->Next;
	free(next);
}

4.3、test.c

#include "list.h"

void TestList1()
{
	SLTNode* plist = NULL;

	SListPushFront(&plist, 1);
	SListPushFront(&plist, 2);
	SListPushFront(&plist, 3);
	SListPushFront(&plist, 4);
	SListPushFront(&plist, 5);
	SListPushFront(&plist, 6);

	SLTNode* pos = SListFind(plist, 3);
	SListEraseAfter(pos);

	SListPrint(plist);

	SListDestroy(&plist);
}

int main()
{
	TestList1();
	return 0;
}

你可能感兴趣的:(数据结构与算法,c语言,链表,开发语言,算法,数据结构)