单链表的概念及实现

单链表的概念

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

链表结构在逻辑上是连续的,但是物理空间不一定是连续的。而现实中的节点一般都是在堆上开辟的。从堆上申请的空间,是按照一定策略来分配的,每次申请的空间位置都是随机的。

而实际中链表的结构特别多,光单链表就分为:

单向链表(单链表)和双向链表(双链表)

带头单链表和不带头单链表

循环链表和非循环

虽然有很多种结构的链表,但是我们最长用到的单链表还是无头单向非循环链表

结构非常简单,一般也不会单独存储数据,实际中更多的作为其他数据结构的子结构,如哈希桶等。

单链表的实现(无头单向非循环链表)

链表节点结构

typedef int SLTDataType;    //存储元素的类型

typedef struct SigList
{
	SLTDataType a;          //存储的数据
	struct SigList* next;   //存储指向下一个位置的地址的指针
}SLTNode;

动态申请一个节点的函数:

SLTNode* BuyNewNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); //动态申请一个节点
	if(newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->a = x;        //将元素放进去
	newnode->next = NULL;  //将指向的下一个元素的指针置空
	return newnode; 
}

然后是最简单的遍历链表

void Print(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)            //下一个不为空 链表就还没到结尾
	{
		printf("%d->", cur->a);    //每个节点打印
		cur = cur->next;
	}
	printf("NULL\n");
}

单链表的尾插(首先我们是传过去的是链表的首地址,那我们需要找到最后一个节点的位置):

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
        SLTNode* tail = *pphead;   //不能直接传过来的地址去找,这样会改变首节点指针的指向
		while (tail->next != NULL) //一直向后找
		{
			tail = tail->next;
		}
}

当我们找到这时候我们得先使用动态申请一个节点,将tail的next指向新节点

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	    SLTNode* newnode = BuyNewNode(x); //新申请的节点
        SLTNode* tail = *pphead;   
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
        tail->next = newnode; //将新节点链接上
}

这样我们就链接了新的节点,但是如果我们这个链表一开始为空,那是不是tail链接下一个节点就出问题了?所以我们就需要直接将新节点变为我们的首节点(所以我们使用的是二级指针,当需要改变穿过来指针的内容,就必须使用地址,而非单指针,因为形参的改变不影响实参

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuyNewNode(x);
	if (*pphead == NULL)        //当这个链表为空链表,新节点就为首节点
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

单链表的头插

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuyNewNode(x); //创建新节点
	
	newnode->next = *pphead;          //新节点指向首节点的地址
	*pphead = newnode;                //首节点指针指向新节点地址
}

根据上面尾插代码和头插代码可以分析出来,单链表更适合头插。

尾删(需要注意链表是否只有一个节点,如果只有一个节点就需要对头指针改变,所以我们传递是头指针的地址(形参的改变不改变实参))

void SLTPopBack(SLTNode** pphead)
{
	assert(*pphead); //如果是个空链表 直接报错
	SLTNode* tail = *pphead;
	if (tail->next == NULL) //判断是否只有一个节点,如果只有一个节点,头指针置空
	{
		free(tail);
		tail = NULL;
		*pphead = NULL;
	}
	else                    //找到最后一个节点的前一个节点,释放最后一个节点
	{                       //将删除节点的前一个节点的next置空
		while(tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

头删(只需要判断链表是否为空):

void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead);        //判断是否为空链表,空链表不能进行删除操作

	SLTNode* head = *pphead; 
	*pphead = head->next;
	free(head);
	head = NULL;
}

不管是否有一个节点,只需要将头指针指向头节点的next即可,然后释放头节点。

所以我们对比尾删和头删,可以发现头删的效率更高,单链表也更加适合头删

查找并返回查找节点地址(只需要遍历链表找到元素相同的节点并返回):

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{                    //链表      //查找的节点元素
	while (phead)
	{
		if (phead->a == x)
		{
			return phead;    //找到返回地址
		}
		phead = phead->next; 
	}
	return NULL;             //找不到返回NULL
}

有了find函数后,我们可以创建根据find找到的(pos)位置来进行插入,我们可以在pos前插入或者pos后插入。

pos位置之前插入(使用SLTFind函数找到pos位置):

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos && *pphead);        //保证链表和pos位置都不为NULL


	SLTNode* newnode = BuyNewNode(x);//创建一个节点
	if (pos == *pphead)        //如果pos位置为头节点,那就是头插
	{                          //我们可以直接调用头插函数或者自己写
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else                    //需要找到pos位置前节点
	{
		SLTNode* cur = *pphead;
		while (cur->next != pos)    
		{
			cur = cur->next;   //cur == pos前一个节点 
		}
		newnode->next = pos;   //新节点的next指向pos节点
		cur->next = newnode;   //cur的next指向新节点,断开了与pos节点的链接
	}
}

pos位置后插入(使用SLTFind函数找到pos位置):

void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);             //判断pos不为NULL
	SLTNode* newnode = BuyNewNode(x); //创建新节点
	newnode->next = pos->next;    //新节点next链接pos位置后一个节点
	pos->next = newnode;          //pos的next链接新节点
}

pos位置后插入我们可以看到只需要考虑新节点的链接和pos的next的链接,如图:

从图可以看出,pos相当于头节点,在头节点后一个位置插入,不需要像pos前插入新节点再去找前一个节点,跟头插有相似处。

插入我们了解,而删除我们可以删除pos位置也可以删除pos后面的位置,所以我们实现这两个函数。

pos位置删除(需要找到pos的前一个节点):

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pos && *pphead);     //首先还是判断链表和pos位置不为NULL

	if (*pphead == pos)        //判断头节点是否为pos
	{                          //pos等于头节点,代表我们需要修改链表头指针
		SLTPopFront(pphead);   //所以我们传的地址,对头指针进行修改
	}                          //直接调用头删函数
	else                        
	{                               //pos不为头节点
		SLTNode* cur = *pphead;    
		while (cur->next != pos)    //找到pos位置前一个节点的地址
		{
			cur = cur->next;
		}
		cur->next = cur->next->next;    //将pos前一个位置节点指向pos后一个位置节点
		free(pos);                      //释放pos空间
	}
}

pos位置后删除:

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);        //只需要判断pos不为NULL
	if (pos->next == NULL)   //pos位置后为空直接返回,没有可以删除的节点
	{
		return;
	}
	else
	{
		SLTNode* cur = pos->next; 
		pos->next = pos->next->next; //将pos的next指向pos的next的next;
		free(cur);            //释放提前存好的pos的next节点
		cur = NULL;
	}
}

其实观察下来可以发现,删除pos后一个节点跟头删有相似之处,不需要查找其他节点,比删除pos位置效率更高一些。

最后我们只需要在结束使用链表时,释放所有节点空间,不造成内存泄漏

void SLTDestroy(SLTNode** pphead)
{
	assert(*pphead);
	SLTNode* cur = *pphead;
	SLTNode* tail = cur;
	while (cur)
	{
		tail = cur->next;
		free(cur);
		cur = tail;
	}
}

注意,我们所使用二级指针的函数判断了指针是否为空,其实我们还需要判断指针是否无效地址。

比如:


void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuyNewNode(x);
	
	newnode->next = *pphead;
	*pphead = newnode;
}

int main()
{
    SLTNode* plist = NULL;
    SLTPushFront(plist); //错误代码
    return 0;
}

这样很容出错,但是又不好找到错误,所以我们可以加上断言;

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
    assert(pphead);     //代码运行时会为我们准确的报错
	SLTNode* newnode = BuyNewNode(x);
	
	newnode->next = *pphead;
	*pphead = newnode;
}

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

无头单向非循环链表实现完整代码

//SigList.h
#pragma once

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

typedef int SLTDataType;

typedef struct SigList
{
	SLTDataType a;
	struct SigList* next;
}SLTNode;

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//打印
void Print(SLTNode* phead);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos);

//pos后面插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//pos位置后面删除
void SLTEraseAfter(SLTNode* pos);

void SLTDestroy(SLTNode** pphead);
//SigList.c

#include "SigList.h"

SLTNode* BuyNewNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if(newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->a = x;
	newnode->next = NULL;
	return newnode;
}

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

	SLTNode* newnode = BuyNewNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead); //报错 说明指针错误 空指针 
	assert(*pphead); //如果是个空链表 直接报错
	SLTNode* tail = *pphead;
	if (tail->next == NULL)
	{
		free(tail);
		tail = NULL;
		*pphead = NULL;
	}
	else
	{
		while(tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

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


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

	SLTNode* newnode = BuyNewNode(x);
	
	newnode->next = *pphead;
	*pphead = newnode;
}

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

	SLTNode* head = *pphead;
	*pphead = head->next;
	free(head);
	head = NULL;
}

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	while (phead)
	{
		if (phead->a == x)
		{
			return phead;
		}
		phead = phead->next;
	}
	return NULL;
}
//pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos && *pphead);


	SLTNode* newnode = BuyNewNode(x);
	if (pos == *pphead)
	{
		newnode->next = *pphead;
		*pphead = newnode;
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		newnode->next = pos;
		cur->next = newnode;
	}
}

//pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos && *pphead);

	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* cur = *pphead;
		while (cur->next != pos)
		{
			cur = cur->next;
		}
		cur->next = cur->next->next;
		free(pos);
	}
}

//pos后面插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuyNewNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

//pos位置后面删除
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	if (pos->next == NULL)
	{
		return;
	}
	else
	{
		SLTNode* cur = pos->next;
		pos->next = pos->next->next;
		free(cur);
		cur = NULL;
	}
}


void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLTNode* cur = *pphead;
	SLTNode* tail = cur;
	while (cur)
	{
		tail = cur->next;
		free(cur);
		cur = tail;
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值