c--单向链表和双向链表

目录

一、链表的概念

1.链表的特点

二、链表详解

1.链表的组成

2.链表的分类

三、单向链表的实现

1.单向链表的结构

2.创建新节点

3.单向链表的尾插、头插

4.单向链表的尾删、头删

5.单向链表的查找

6.单向链表在指定位置前 / 后的插入

7.单向链表删除指定节点 / 指点节点后面的节点

8.单向链表的销毁

9.整体代码展示

四、带头双向循环链表的实现

1.带头双向循环链表的结构

2.带头双向循环链表创建新节点

3.带头双向循环链表的初始化及销毁

4带头双向循环链表的尾插、头插

5.带头双向循环链表的尾删、头删

6.带头双向循环链表的查找

7.带头双向循环链表在指定位置后插入

8.带头双向循环链表在指定位置后删除

 9.整体代码展示

五、单向链表和带头双向循环链表的区别

1.结构

2.访问方式

3.空间占用

4.插入和删除操作

六、顺序表和链表的优缺点


一、链表的概念

  • 链表是一种基本的数据结构,用于存储一系列元素。与数组不同,链表的元素在内存中不是连续存放的,而是由一系列节点组成,每个节点包含数据和一个或多个指向下一个节点的指针(或引用)。
  • 链表的物理地址并不相同,就像一节一节的火车一样,通过指针进行连接。每个节点独立存在,并不会影响其他的节点。​​​​​

(每个节点的修改,重新连接并不会影响整体的的链表结构)

1.链表的特点

  1. 动态大小:链表的大小可以动态变化,可以根据需要增加或者删除节点,而无需事先定义容量。而顺序表的本质是数组,数组的空间大小是相对固定的,如果对数组进行头插或者尾插等操作时可能会需要扩容,而扩容就会造成空间上的浪费。
  2. 非连续存储:链表的节点在内存中不必连续存储,每个节点通过指针链接到下一个节点,因此可以在内存中任意位置分配。

  3. 节点结构:链表由节点组成,每个节点包含数据部分和一个或多个指针部分(指向下一个节点或前一个节点)。

  4. 插入和删除效率:在链表中,插入和删除操作通常比数组更高效,因为只需调整指针,而不需要移动其他元素。

  5. 访问效率:链表的随机访问效率较低。要访问某个特定位置的节点,必须从头节点开始逐个遍历,时间复杂度为 O(n)。

  6. 多样性:链表有多种变体,如单链表、双向链表、循环链表等,适用于不同的应用场景。

二、链表详解

1.链表的组成

链表的组成:节点。

  1. 链表是由一个个节点组成的
  2. 每个节点由含有两个部分:存储数据的部分和存储下一个节点地址的部分

2.链表的分类

  1. 单向和双向

  2. 带头和不带头

  3. 循环和非循环

(这三种类别的链表可以相互组合,例如双向带头链表、双向循环链表、带头双向循环链表.........)

三、单向链表的实现

1.单向链表的结构

  • 结构体的创建
typedef int SLDataType; //这样做能便于数据类型的修改

typedef struct SListNode
{
	SLDataType data;//存放数据
	struct SListNode* next;//指向下一个指针
}SLTNode;

2.创建新节点

  • 因为链表的很多操作都需要传建一个新链表(也就是开辟一个新空间),所以我们可以把他写成一个函数,方便其他函数的使用
SLTNode* SLTBuyNode(SLDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//新建一个链表
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

3.单向链表的尾插、头插

  • 单向链表的尾插
void SLTPushBack(SLTNode** pphead, SLDataType x)
{
	assert(pphead);

	SLTNode* newnode = SLTBuyNode(x);

	//如果链表为空
	if (*pphead==NULL)
	{
		*pphead = newnode;
		return;
	}
	//链表不为空,新建一个链表找尾节点
	SLTNode* ptail = *pphead;
	while (ptail->next)
	{
		ptail=ptail->next;
	}
	ptail->next = newnode;
}
  • 单向链表的头插
void SLTPushFront(SLTNode** pphead, SLDataType x)
{
	assert(pphead);

	//链表为空或者不为空,情况都是一样
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

注意:在对链表进行增删等操作时,我们需要用到二级指针。因为考虑到传值或者传址操作,普通的数据在函数调用时需要用一级指针来进行传址;同样地,因为链表本身是一级指针,所以要二级指针来进行传址操作

4.单向链表的尾删、头删

  • 单向链表的尾删
void SLTPopback(SLTNode** pphead)
{
	assert(pphead);
	
	//链表不能为空
	assert(*pphead);

	//如果链表只有一个节点
	if (*pphead == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//如果链表有多个节点
	SLTNode* ptail = *pphead;
	SLTNode* prve = NULL;
	//找到NULL的前一个节点
	while (ptail->next)
	{
		prve = ptail;
		ptail = ptail->next;
	}
	prve->next = NULL;
	free(ptail);
	ptail = NULL;
}
  • 单向链表的头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);

	//链表不能为空
	assert(*pphead);

	//让第二个节点成为头节点,释放第一个节点
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

5.单向链表的查找

SLTNode* SLTFind(SLTNode* phead, SLDataType x)
{
	SLTNode* pcur = phead;

	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

6.单向链表在指定位置前 / 后的插入

  • 在指定位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
	assert(pphead);
	assert(pos);
	//链表不能为空
	assert(*pphead);

	SLTNode* newnode = SLTBuyNode(x);
	//如果在头节点前插入
	if (*pphead == pos)
	{
		//用头插
		SLTPushFront(pphead,x);
		return;
	}
	//不在头节点
	SLTNode* pcur = *pphead;
	//到pos的前一个
	while (pcur->next != pos)
	{
		pcur = pcur->next;
	}
	newnode->next = pos;
	pcur ->next = newnode;
}
  • 在指定位置后插入
void SLTInsertAfter(SLTNode*pos, SLDataType x)
{
	assert(pos);

	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

7.单向链表删除指定节点 / 指点节点后面的节点

  • 删除指定节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	//节点不能为空
	assert(*pphead);

	//如果删除的是头节点
	if (pos == *pphead)
	{
		//执行头删操作;
		SLTPopFront(pphead);
		return;
	}
	//不是头节点
	SLTNode* prve = *pphead;
	while (prve->next!=pos)
	{
		prve = prve->next;
	}
	prve->next = pos->next;
	free(pos);
	pos = NULL;
}
  • 删除指定节点后面的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	//pos->next不能为空
	assert(pos->next);

	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;

}

8.单向链表的销毁

void SListDesTroy(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLTNode* pcur = *pphead;
	
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
}

9.整体代码展示

  • SeList.h
#pragma once

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

typedef int SLDataType;

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

//链表打印
void SLTPrint(SLTNode *phead);

//链表的尾插
void SLTPushBack(SLTNode** pphead, SLDataType x);
//链表的头插
void SLTPushFront(SLTNode** pphead, SLDataType x);
//链表的尾删
void SLTPopback(SLTNode** pphead);
//链表的头删
void SLTPopFront(SLTNode** pphead);

//链表的查找
SLTNode* SLTFind(SLTNode* phead, SLDataType x);

//在指定位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);
//在指定位置后插入
void SLTInsertAfter(SLTNode*pos, SLDataType x);

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);

//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);

//删除链表
void SListDesTroy(SLTNode** pphead);
  • SeList.c
#include"Selist.h"

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

SLTNode* SLTBuyNode(SLDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

void SLTPushBack(SLTNode** pphead, SLDataType x)
{
	assert(pphead);

	SLTNode* newnode = SLTBuyNode(x);

	//如果链表为空
	if (*pphead==NULL)
	{
		*pphead = newnode;
		return;
	}
	//链表不为空,新建一个链表找尾节点
	SLTNode* ptail = *pphead;
	while (ptail->next)
	{
		ptail=ptail->next;
	}
	ptail->next = newnode;
}

void SLTPushFront(SLTNode** pphead, SLDataType x)
{
	assert(pphead);

	//链表为空或者不为空,情况都是一样
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

void SLTPopback(SLTNode** pphead)
{
	assert(pphead);
	
	//链表不能为空
	assert(*pphead);

	//如果链表只有一个节点
	if (*pphead == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//如果链表有多个节点
	SLTNode* ptail = *pphead;
	SLTNode* prve = NULL;
	//找到NULL的前一个节点
	while (ptail->next)
	{
		prve = ptail;
		ptail = ptail->next;
	}
	prve->next = NULL;
	free(ptail);
	ptail = NULL;
}

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead);

	//链表不能为空
	assert(*pphead);

	//让第二个节点成为头节点,释放第一个节点
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

SLTNode* SLTFind(SLTNode* phead, SLDataType x)
{
	SLTNode* pcur = phead;

	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{
	assert(pphead);
	assert(pos);
	//链表不能为空
	assert(*pphead);

	SLTNode* newnode = SLTBuyNode(x);
	//如果在头节点前插入
	if (*pphead == pos)
	{
		//用头插
		SLTPushFront(pphead,x);
		return;
	}
	//不在头节点
	SLTNode* pcur = *pphead;
	//到pos的前一个
	while (pcur->next != pos)
	{
		pcur = pcur->next;
	}
	newnode->next = pos;
	pcur ->next = newnode;
}

void SLTInsertAfter(SLTNode*pos, SLDataType x)
{
	assert(pos);

	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	//节点不能为空
	assert(*pphead);

	//如果删除的是头节点
	if (pos == *pphead)
	{
		//执行头删操作;
		SLTPopFront(pphead);
		return;
	}
	//不是头节点
	SLTNode* prve = *pphead;
	while (prve->next!=pos)
	{
		prve = prve->next;
	}
	prve->next = pos->next;
	free(pos);
	pos = NULL;
}


void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	//pos->next不能为空
	assert(pos->next);

	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;

}

void SListDesTroy(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLTNode* pcur = *pphead;
	
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
}
  • text.c
#include"SeList.h"

//void text1()
//{
//	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
//	node1->data = 1;
//	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
//	node2->data = 2;
//	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
//	node3->data = 3;
//
//	node1->next = node2;
//	node2->next = node3;
//	node3->next = NULL;
//	SLTNode* plist = node1;
//	SLTPrint(plist);
//}

void text2()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPrint(plist);


	SLTPushFront(&plist, 4);
	SLTPrint(plist);

	SLTPopback(&plist);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLTNode* Find=SLTFind(plist, 2);
	if (Find)
	{
		printf("找到了!\n");
	}
	else
	{
		printf("未找到!\n");
	}

	SLTInsert(&plist, Find, 2);
	SLTPrint(plist);

	SLTInsertAfter(Find, 3);
	SLTPrint(plist);

	SLTErase(&plist, Find);
	SLTPrint(plist);

	SLTNode* Find1 = SLTFind(plist, 1);
	SLTEraseAfter(Find1);
	SLTPrint(plist);
}

int main()
{
	//text1();
	text2();
	return 0;
}

四、带头双向循环链表的实现

1.带头双向循环链表的结构

注意:这里的“带头”跟前面我们说的“头节点”是两个概念,实际前面的在单链表阶段称呼不严谨,但是为了更好的理解就直接称为单链表的头节点。 带头链表里的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这里“放哨的”。

“哨兵位”存在的意义:

  1. 遍历循环链表避免死循环。 
  2. 减少代码的复杂性
typedef int LTDataType;

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* prev;//指向上一个节点
	struct ListNode* next;//指向下一个节点
}LTNode;

 

2.带头双向循环链表创建新节点

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	newnode->data = x;
	newnode->next = newnode->prev = newnode;
	return newnode;
}

3.带头双向循环链表的初始化及销毁

  • 带头双向循环链表的初始化,也就是先创造一个哨兵位
LTNode* LTInit(LTNode* phead)
{
	 phead = LTBuyNode(-1);
	
	phead->next = phead->prev = phead;//前一个节点和后一个节点都指向自己
	
}
  •  带头双向循环链表的销毁
void LTDestory(LTNode* phead)
{
	LTNode* del = phead->next;

	while (del != phead)
	{
		LTNode* next = del->next;
		free(del);
		del = next;
	}
	free(phead);
	phead = NULL;
}

在这里我们可以发现,为什么单向链表需要用到二级指针,而带头双向循环链表就不需要呢?

  • 单向链表是没有哨兵位的,所以需要借助外部创建好的结构体指针变量来访问,因为是一级指针,所以要用到二级指针来进行对单向指针的增删操作。
  • 而带头双向循环链表中已经有一个哨兵位了,所以我们不需要用外来的结构体指针变量来访问,直接通过哨兵位进行后续的操作。

4带头双向循环链表的尾插、头插

  • 带头双向循环链表的尾插
void LTPushBack(LTNode* phead,LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead;
	newnode->prev = phead->prev;

	phead->prev->next = newnode;
	phead->prev = newnode;
}
  • 带头双向循环链表的头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode->next;
	phead->next = newnode;
}

5.带头双向循环链表的尾删、头删

  • 带头双向循环链表的尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	//双向链表不能只有一个哨兵位
	assert(phead->next != phead);

	LTNode* del = phead->prev;

	del->prev->next = phead;
	phead->prev = del->prev;

	free(del);
	del = NULL;
}
  • 带头双向循环链表的头删
void LTPopFront(LTNode* phead)
{
	assert(phead);
	//双向链表不能只有一个哨兵位
	assert(phead->next != phead);

	LTNode* del = phead->next;

	phead->next = del->next;
	del->next->prev = phead;
}

6.带头双向循环链表的查找

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	//phead是哨兵位,为-1。要从哨兵位的下一个开始找
	LTNode* find = phead->next;
	while (find != phead)
	{
		if (find->data == x)
		{
			return find;
		}
		find = find->next;
	}
	return NULL;
}

7.带头双向循环链表在指定位置后插入

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;
}

8.带头双向循环链表在指定位置后删除

void LTErase(LTNode* pos)
{
	LTNode* del = pos;

	del->next->prev = del->prev;
	del->next = del->next->next;
}

 9.整体代码展示

  • List.h
#pragma once

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

typedef int LTDataType;

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

//双向链表的初始化和销毁
LTNode* LTInit(LTNode* phead);
void LTDestory(LTNode* pphead);

//双向链表的打印
void LTPrint(LTNode* phead);

//双向链表的尾插
void LTPushBack(LTNode* phead,LTDataType x);
//双向链表的头插
void LTPushFront(LTNode* phead, LTDataType x);

//双向链表的尾删
void LTPopBack(LTNode* phead);
//双向链表的头删
void LTPopFront(LTNode* phead);

//查找双向链表
LTNode* LTFind(LTNode* phead, LTDataType x);

//在pos后插入
void LTInsert(LTNode* pos, LTDataType x);

//在pos后删除
void LTErase(LTNode* pos);
  • List.c
#include"list.h"

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	newnode->data = x;
	newnode->next = newnode->prev = newnode;
	return newnode;
}


LTNode* LTInit(LTNode* phead)
{
	 phead = LTBuyNode(-1);
	
	phead->next = phead->prev = phead;
	
}

void LTDestory(LTNode* phead)
{
	LTNode* del = phead->next;

	while (del != phead)
	{
		LTNode* next = del->next;
		free(del);
		del = next;
	}
	free(phead);
	phead = NULL;
}

void LTPrint(LTNode* phead)
{
	//phead是哨兵位,不需要打印
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

void LTPushBack(LTNode* phead,LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead;
	newnode->prev = phead->prev;

	phead->prev->next = newnode;
	phead->prev = newnode;
}

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode->next;
	phead->next = newnode;
}

void LTPopBack(LTNode* phead)
{
	assert(phead);
	//双向链表不能只有一个哨兵位
	assert(phead->next != phead);

	LTNode* del = phead->prev;

	del->prev->next = phead;
	phead->prev = del->prev;

	free(del);
	del = NULL;
}


void LTPopFront(LTNode* phead)
{
	assert(phead);
	//双向链表不能只有一个哨兵位
	assert(phead->next != phead);

	LTNode* del = phead->next;

	phead->next = del->next;
	del->next->prev = phead;
}


LTNode* LTFind(LTNode* phead, LTDataType x)
{
	//phead是哨兵位,为-1。要从哨兵位的下一个开始找
	LTNode* find = phead->next;
	while (find != phead)
	{
		if (find->data == x)
		{
			return find;
		}
		find = find->next;
	}
	return NULL;
}

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);

	newnode->next = pos->next;
	newnode->prev = pos;

	pos->next->prev = newnode;
	pos->next = newnode;
}

void LTErase(LTNode* pos)
{
	LTNode* del = pos;

	del->next->prev = del->prev;
	del->next = del->next->next;
}
  • text.c
#include"List.h"

void text1()
{
	//初始化双向链表
	LTNode* plist1=NULL;
	LTNode* plist=LTInit(plist1);
	
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);

	LTPrint(plist);

	LTPushFront(plist, 4);
	LTPrint(plist);

	LTPopBack(plist);
	LTPrint(plist);

	LTPopFront(plist);
	LTPrint(plist);

	LTNode* find = LTFind(plist, 3);
	if (find)
	{
		printf("找到了!");
	}
	else
	{
		printf("未找到!");
	}
	printf("\n");

	LTInsert(find, 4);
	LTPrint(plist);

	LTErase(find);
	LTPrint(plist);
}


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

五、单向链表和带头双向循环链表的区别

1.结构

  • 单向链表:每个节点包含一个数据域和一个指向下一个节点的指针(即“下一个”指针)。链表的头节点指向第一个存储的节点。
  • 带头双向循环链表:每个节点包含一个数据域、一个指向下一个节点的指针(“下一个”指针)和一个指向前一个节点的指针(“前一个”指针)。带头的双向链表通常有一个头节点,头节点不存储有效数据,而是用于简化对链表的操作。

2.访问方式

  • 单向链表:只能从头节点进行遍历,不能从后往前遍历。
  • 带头双向循环链表:既可以从后往前,也可以从前往后遍历。

3.空间占用

  • 单向链表:每个节点只有一个指向下一个节点的指针,存在较小的内存消耗
  • 带头双向循环链表:每个节点既有指向下一个节点的指针,也有指向前一个节点的指针,所以内存消耗较大

4.插入和删除操作

  • 单向链表:插入和删除操作需要考虑前驱节点,对于删除操作,通常需要从头开始遍历找到前驱节点。
  • 带头双向循环链表:由于每个节点都有对前驱节点的引用,可以更方便地执行插入和删除操作,无需前向遍历。

六、顺序表和链表的优缺点

优点 缺点

顺序表

1. 物理空间是连续的,方便用下标随机访问。
2. CPU高速缓存命中率会更高。
1. 由于需要物理空间连续,空间不够需要扩容。其次扩容机制还存在一定的空间浪费。
2. 头部或者中部插入、删除、挪动数据效率低 O(N)。
链表 1、按需申请和释放空间。
2. 任意位置插入、删除数据效率高 O(1)。
1. 不支持下标的随机访问。有些算法不适合在它上面进行。如:二分、排序等