数据结构——链表

目录

  • 一、 链表
    • 1.链表的概念及结构
    • 2.链表的基本特点
  • 二、链表的分类
    • 1.单链表的接口
    • 2.单链表的基本操作(接口)的实现
      • 深度理解cur = cur->next
    • 3.双向循环链表
      • (1)初始化问题
      • (2)双向链表指针更改顺序
  • 三、assert的场景
  • 四、总结


一、 链表

1.链表的概念及结构

链表是一种常见的数据结构,用于线性方式存储数据,链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表是一个一个内存块,用指针把它们链接起来。
链表适用于当数据结合频繁变化,需要快速插入和删除,内存空间分散的场景。

数据结构——链表_第1张图片

上 一个节点存下一个节点的地址,每一个节点的类型都是一个结构体,这里每个结构体里面不是结构体,是个指针,这个指针的类型是struct SListNode。

简单定义一下

//为了后续能方便替换成其他类型,如果直接在结构体里用int可能写的多了会混淆,也是好分辨一点
typedef int SLTDataType;			

struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
};


//因为这里每次用到结构体类型struct SListNode比较长所以用typedef把它定义短一点
//可以单独写一行定义
typedef struct SListNode SLTNode;

//也可以直接在定义链表时用typedef,这两种写法是等价的
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

2.链表的基本特点

动态内存分配:链表中的节点是在运行时动态创建的,可以根据需要分配和释放内存。
可扩展性:链表的大小可以根据需要动态增长或缩小,没有固定的大小限制。
非连续存储:链表中的节点可以分散在内存的不同位置,每个节点包含数据和指向下一个节点的指针。

二、链表的分类

数据结构——链表_第2张图片虽然有这么多链表的结构,实际中最常用的还是两种结构。

数据结构——链表_第3张图片

1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
⒉.带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

常见的链表就有不带头单向非循环链表带头双向循环链表

1.单链表的接口

单链表(不带头单向非循环链表)

插入:在链表的特定位置添加新节点。
删除:移除链表中的特定节点。
搜索:查找链表中的特定值。
遍历:按顺序访问链表的所有节点。

定义

//假设这里为头文件,这里放结构体和函数的声明
//SList.h

#pragam once
#includ <stdio.h>
#include 
#include 

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;


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

//因为涉及到phead指针的修改,所以参数得传二级指针
void SLTPushBack(SLTNode** pphead, SLTDataType x);		//链表的尾插
void SLTPushFront(SLTNode** pphead ,SLTDataType x);		//链表的头插
void SLTPophBack(SLTNode** pphead);						//链表的尾删
void SLTPopFront(SLTNode** pphead);						//链表的头删

//单链表查找,通常也可以用来简单修改一下值
SLTNode* SLTFind(SLTNode*phead, SLTDataType x)void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x);			//在指定位置之前插入数据 
void SLTInsertAfter(SLTNode** pphead,SLTNode* pos,SLTDataType x);		//在指定位置之后插入数据 
void SLTErase(SLTNode** pphead,SLTNode* pos);							//删除pos节点
void SLTEraseAfter(SLTNode** pphead,SLTNode* pos);						//删除pos之后节点
void SLTDestroy(SLTNode** pphead)//销毁链表

2.单链表的基本操作(接口)的实现

#include "SList.h"

//创建新节点,需要新节点时直接调用该函数就行了
SLTNode* BuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTDataType*)malloc(sizeof(SLTDataType));
	if(newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}	
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

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

//原尾节点中要存储新的尾节点的地址
void SLTPushBack(STLNode** pphead,SLTDataType x)
{
	 asssert(pphead);						//pphead也就是*pphead的地址,pphead不能为空,如果pphead为空了那么*pphead地址在哪		
	 SLTNode* newnode = BuyNode(x);
	 if(tail->next == NULL)					//空链表
	 {
		*pphead = newnode;
	 }
	 else									//链表不为空
	 {
	 	SLTNode* tail = NULL;
	 	while(tail->next != NULL)
	 	{
	 		tail = tail->next;
	 	}
	 	tail->next = x;
	 }
}

//把新节点链接在链表头部
void SLTPushFront(SLTNode** pphead,SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuyNode(x);
	newnode->next = *pphead;
	*pphead->next = newnode;	
}

//要注意判断只有一个节点的情况和判断链表为空的情况
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);						//空链表
	if((*pphead)->next == NULL)				//只有一个节点
	{
		free(*pphead);
		*pphead = NULL;	
	}
	else									//多个节点
	{
		SLTNode* tail = *pphead;
		while(tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
		

		/*或在定义一个变量,两种方法都可以
		SLTNode* prev = NULL;
		SLTNode* tail = *pphead;
		while(tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL; 
		prev = NULL;
		*/
	}
}

//删除头节点前要把下一个节点存储起来
void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* first = *pphead;
	*pphead = first->next;
	free(first);
	first = NULL;
}

//遍历链表
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	//空节点返回null,不用assert
	SLTNode* cur = phead;
	while(cur)
	{
		if(cur->data == x)		 //找cur成员里有没有x
		{	
			return cur;						
		}
		cur = cur->next;		
	}
	return NULL;			   //如果以上都没有返回值,则找不到,返回空指针
}

//1.注意判断空链表;不能在空节点前插入(尾插)。2.pos不能为空。3.pos是头节点的情况。
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x)
{

	assert(pos);				//pos不能为空	

	//pos是头节点的情况,直接调用头插函数
	if(pos == *pphead)	
	{
		SLTPushFront(pphead,x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while(prve->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuyNode(x);
		newnode->next = pos;
		prev->next = newnode;
	}
}

//删除pos节点
void SLTErase(SLTNode** pphead,SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	if(*pphead == pos)					//如果pos是头指针
	{
		SLTPopFront(pphead,pos)//调用头删函数
	}
	else
	{
		SLTNode* prev = *pphead;
		while(prev->next != pos)		//找到pos的前一个位置
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

//在pos后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuyNode(x);

	//下面两行顺序不能乱
	newnode->next = pos->next;
	pos->next = newnode;
}

//删除pos后的数据,注意判断pos之后的节点是不是空节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLTNode* del = pos->next;
	pos->next = pos->next->next;
	free(del);
	del = NULL;
}

//销毁链表
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = pcur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
 }

测试函数

#include "SList.h"
void test1()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist,1);
	SLTPushBack(&plist,2);
	SLTPushBack(&plist,3);
	SLTPushBack(&plist,4);
	SLTPrint(plist);					//打印:1->2->3->4->NULL

	SLTPopBack(&plist);
	SLTPrint(plist);					//打印:1->2->3->NULL			

}
void test2()
{
	SLTNode* plist;
	SLTPushFront(&plist,1);
	SLTPushFront(&plist,2);
	SLTPushFront(&plist,3);
	SLTPushFront(&plist,4);
	SLTPrint(plist);
	
	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPrint(plist); 					//打印:2->1->NULL
}

void test3()
{
	SLTNode* plist=NULL;
	SLTPushBack(&plist,1);
	SLTPushBack(&plist,2);
	SLTPushBack(&plist,3);
	SLTPushBack(&plist,4);
	SLTPrint(plist);
	
	//把值为2的节点乘2
	SLTNode* ret = SLTFind(plist,3);
	ret->data *= 2;
	SLTPrint(plist);					//打印:1->2->6->4->NULL
	
	SLTInsert(&plist,ret,66);			//ret的位置是查找函数x的值
	SLTPrint(plist);					//打印:1->2->66->6->4->NULL
	
	//后面也类似就不演示了
}
int main()
{
	test1();
	test2();
	test3();
	return 0;
}

深度理解cur = cur->next

数据结构——链表_第4张图片

数据结构——链表_第5张图片
一定要理解物理结构,等我们熟练了之后通常用逻辑结构

数据结构——链表_第6张图片

3.双向循环链表

在这里插入图片描述

循环的特点:
1.尾next指向哨兵位的头
2.哨兵位的头prev指向尾

有了单链表的基础,这里我们就直接定义了

//List.h

#pragma once
#include 
#include 
#include 
#include 

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

void LTInit(LTNode* phead);
void LTDestroy(LTNode* phead);
void LTPrint(LTNode* phead);
bool LTEmpty(LTNode* phead);			//判断链表为空

//因为这里是直接改的指针变量所以不用二级
void LTPushBack(LTNode* phead,LTDataType x);
void LTPushFront(LTNode* phead,LTDataType x);
void LTPopBack(LTNode* phead);
void LTPopFront(LTNode* phead);

void LTInsert(LTNode* pos,LTDataType x);		
void LTErase(LTNode* phead);					
void LTDestroy(LTNode* phead);					//遍历链表,逐个销毁,然后销毁哨兵位就行了

实现

#include "List.h"
LTNode* BuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if(newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
		//exit(-1);			或使用这个终止程序
	}
	newnode->prev = NULL;
	newnode->next = NULL;
	newnode->data = x;
	return newnode;
}

bool Empty(LTNode* phead)
{
	assert(phead);
	return phead == phead->next;				//如果phead->next等于phead那就是空,相等就是真,真就是空
}

void LTInit(LTNode* phead)
{
	LTNode* Phead = BuyNode(-1);
	phead->prev  = phead->next = phead;		//双向链表是带头双向循环的,所以初始情况哨兵节点前指针和后指针要指向自己,不然不是循环
	return phead;
}

void LTPrint(LTNode* phead)
{
	printf("<=>");
	LTNode* cur = phead;
	while(cur)
	{	
		printf("%d<=>",cur->data);
		cur = cur->next;
	}
	printf("\n");
}

//双向链表是空链表时,指的是只有一个哨兵位
//保证链表不是空链表
void LTPushBack(LTNode* phead,LTDataType x)
{
	assert(phead);
	LTNode* newnode = BUyNode(x);
	LTNode* tail = phead->prev;
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

void LTPushFront(LTNode* phead,LTDataType x)
{
	LTNode* newnode = BuyNode(x);
	newnode->next = phead->next;
	phead->next->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;
}

void LTPopBack(LTNode* phead);
{	
	assert(phead);
	assert(!Empty(phead)); 		//断言它不等于空 
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	tailPrev->next = phead;
	phead->prev = tailPrev;
	free(tail);
	tail = NULL;
}

void LTPopFront(LTNode* phead)
{	
	assert(phead);
	LTErase(phead->next);
}

void LTInsert(LTNode* pos,LTDataType x)
{
	assert(pos);
	LTNode* prev = pos->prev;
	LTNode* newnode = BuyNode(x);
	prev->next = newnode;
	newnode->prev = prev;
	newnode->next = pos;
	pos->prev = newnode;
}

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

void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}
#include "List.h"

void test1()
{
	LTNode* plist = LTInit();
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);						//打印:<=phead=>1<=>2<=>3<=>4<=>

	LTPopBack(plist);
	LTPrint(plist);						//打印:<=phead=>1<=>2<=>3<=>
}
void test2()
{
	LTNode* plist = LTInit();
	LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);
	LTPrint(plist);
}
void test3()
{
	LTNode* plist = LTInit();
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);
	LTPrint(plist);						//打印:<=phead=>1<=>2<=>3<=>4<=>
	LTNode* pos = LTFind(plist, 2);		//找到链表里值为2的
	if (pos)							//如果链表中有该值
	{
		LTErase(pos);					//删除2
		pos = NULL;
	}
	LTPrint(plist);						//打印:<=phead=>1<=>3<=>4<=>
	LTPopFront(plist);
	LTPrint(plist);						//打印:<=phead=>3<=>4<=>

	LTDestroy(plist);
	plist = NULL;						//在调用函数的这里置空指针
}
int main()
{
	test1();
	test2();
	test3();
	return 0;
}

(1)初始化问题

因为单链表是不带头不循环,链表最开始没有插入数据链表为空初始状态,直接赋值为NULL就好了没必要单独写个函数 。
顺序表单独写个初始化函数是因为:顺序表为空的标志是size=0;capacity看需不需要开空间,不开空间等于0,指针为NULL;开空间的话还要malloc检查,有很多事的就交给函数去做。

(2)双向链表指针更改顺序

数据结构——链表_第7张图片其他删除等都类似或在多定义一个指针变量也可以不按顺序链接。

三、assert的场景

在一定不能为空的情况下使用assert ,参数传错,断言是进行一些比较明显典型错误的前期检查。
为什么会有pphead?
如果头插头删等这些需要动指针光传plist不可以,得传plist的地址,只要传plist的地址就有pphead,所以只要用pphead的地方都要进行断言防止传错,例如一级传给二级指针。

四、总结

数据结构——链表_第8张图片

1:链表的主体是一个一个的节点每个节点用指针链接起来,访问链表要用指针找链表的内容。
2:单链表和双向链表的插入函数和删除函数也可以用来头插、头删、尾插、尾删。
3:顺序表是在一个数组里,在数组里我们要知道容量多大,实际数据个数多少,所以用了一个结构体,结构体里有个指针指向顺序表,但是它除了指针还需要有size和capacity,size用来标识有多少数据,capacity用来进行扩容。
本质都是指针,需要加size知道有多少数据,需要加capacity方便扩容,而链表不需要size遇到NULL就结束了只需要有一个指针指向第一个节点。

你可能感兴趣的:(数据结构,链表)