<数据结构与算法>单链表

本文详细介绍了链表的基本概念、结构以及如何实现链表的插入、删除、打印等操作。通过示例代码展示了链表的头插、尾插、查找、前插入、删除、后插入和后删除等功能,并提供了完整代码实现。同时,文中强调了在处理链表时需要注意的指针管理和内存分配问题。

 

 

目录

前言

一、链表的概念及结构

二、链表各函数的实现

结构体定义 

SLTPrint  打印

SLTPushBack 尾插

 SLTPushFront 头插

 SLTPopBack 尾删

SLTPopFront  头删

SListFind 查找

 SListInsert 前插入

 SLTErase 删除

SLTInsertAfter 后插入

SLTEraseAfter 后删除

SLTDestroy 释放 

三、完整链表代码

1.SList.h 

2.SList.c

3. Test.c

总结


前言

由顺序表的问题及思考,我们又有了链表这一概念。


一、链表的概念及结构

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

 

二、链表各函数的实现

结构体定义 

  •  在定义结构体时,struct SListNode才是结构体类型名,在未完成重命名操作前,不能使用重命名后的名字
typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;//在定义结构体时,struct SListNode才是结构体类型名,在未完成重命名操作前,不能使用重命名后的名字
}SLTNode;

SLTPrint  打印

  • 不要看到指针就断言,这里的打印函数不需要assert断言,因为链表的数据是存放在结构体中的,为空同样可以打印,而之前学过的顺序表打印时则需要断言,这是因为它的数据不是存放在结构体中的,而是存放在一个数组空间,该结构体是必须存在的,由结构体内的指针存放该空间的地址,结构体内有指向数组的指针、size、capacity,如果不断言,那么就可能出现错误(数组内容是否为空是由size决定的)
  • 遍历链表时,不能使用指针++因为链表内每个节点都是单独malloc出来的,地址是不连续的,如果整个空间都是连续的那么指针++就可以跳过一个结构体大小的字节,就可以实现遍历。
  • 循环条件不能为cur -> next != NULL 因为这样会丢掉最后一个数据 
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	//while (cur->next != NULL)
	//while(cur != NULL)
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
		//cur++;
	}
	printf("NULL\n");
}

SLTPushBack 尾插

  • 首先开辟新节点,强转为结构体指针类型,并判断是否成功开辟
  • 如果链表不为空,寻找尾节点,该节点的next指针指向NULL,循环条件不能写为 tail != NULL 因为这样循环会使tail指向到NULL,应为tail -> next != NULL
  • 找到后不能写为 tail = newnode 因为tail只是一个局部变量,将新节点地址赋值给tail毫无用处,tail销毁后什么也没有改变,实际上应为 tail ->next  = newnode 这样赋值才是真正的串联起尾节点与新插入的节点将新节点的地址赋值给上一个节点的next 
  • 尾插本质:原尾节点中要储存新尾节点的地址
  • 如果链表为空,即phead == NULL 那么就把新开辟的节点赋值给 phead,但是一定要注意,我们在主函数中会创建 SLTNode * plist == NULL,把它作为头节点,如果要改变plist指针的值,我们要传该指针的地址!即二级指针。
  • 但是在后面要改变结构体内的next指针进行链接时,不需要再使用二级指针,因为我们要改变是结构体,使用结构体指针即可

例如:

void Func(int* ptr)
{
    ptr = (int* )malloc(sizeof(int));
}

int main()
{
    int *px = NULL;
    Func(px);
    return 0;
}

对ptr开辟空间不会改变px的值 

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

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

	SLTNode* newnode = BuySLTNode(x);

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		// 找尾
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}

		tail->next = newnode;
	}
}

 SLTPushFront 头插

  • 头插依旧需要传二级指针,因为必有一种情况(链表为空)需要改变头节点的值。
  • 链表为空或不为空,头插操作一样
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);

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

 SLTPopBack 尾删

  • 删除时要找到尾的上一个节点,将该节点的next 赋值为NULL,不能直接将尾节点释放,这样的话会使倒数第二个指针成为野指针
  • 若只有一个数据,上面程序就不适合了,需要单独讨论,释放头节点并置为空
  • 若没有数据,温柔/暴力检查

 

void SLTPopBack(SLTNode** pphead)
{
	// 暴力检查
	assert(pphead);
	assert(*pphead);

	// 温柔的检查
	//if (*pphead == NULL)
	//	return;

	// 1、只有一个节点
	// 2、多个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		// 找尾
		//SLTNode* prev = NULL;
		//SLTNode* tail = *pphead;
		//while (tail->next != NULL)
		//{
		//	prev = tail;
		//	tail = tail->next;
		//}

		//free(tail);
		//tail = NULL;

		//prev->next = NULL;

		SLTNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}

		free(tail->next);
		tail->next = NULL;
	}
}

SLTPopFront  头删

  • 只用分为俩种情况,大于等于一个数据、没有数据
  • 创建新的结构体指针first,用于存储第一个结构体在释放第一个结构体空间时,释放first即可,不能释放pphead

void SLTPopFront(SLTNode** pphead)
{
	// 暴力检查
	assert(pphead);
	assert(*pphead);

	// 温柔的检查
	//if (*pphead == NULL)
	//	return;

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

SListFind 查找

  • 定义结构体指针,遍历链表,找到值与给定的相等的节点,并返回该节点地址
  • 修改链表中某节点的值一般都要使用查找函数
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

 SListInsert 前插入

  • 如果要插入的位置是第一个时,就是头插在某位置插入,是在该位置之前,所以不可能尾插)
  • 否则,就找到该节点的上一个节点,进行插入操作
  • 二级指针pphead必不为空,因为它的值是一级指针plist的地址,一般情况下它是不为空的,但是以防万一有错误的传参,所以要断言。
  • 要找的结构体指针pos也不能为空,这样会导致找不到该节点,所以也需要断言。

是否断言需要分析:

  • 空链表可以打印,不需要断言链表指针
  • 空链表可以插入,不需要断言链表指针
  • 空链表不能删除,需要断言链表指针
// pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	assert(pphead);

	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		// 找到pos的前一个位置
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

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

 SLTErase 删除

  • 首先要断言pphead和pos,又因为断言pos的同时,间接的断言了链表不为空,所以可以不用断言 *pphead,但是为了代码更健壮,我们加上 *pphead 的断言
  • free后再本函数内置空没有用,因为此时形参改变不了实参的值,所以可以让使用者再外面使用删除函数后自己置空,也可以传二级指针pos,在函数内使其置空

面试题: 可以在不使用头指针情况下实现在指定节点前插入功能吗?

        可以,在该节点后插入新节点,然后交换两节点数据,即可实现前插功能

那么能实现删除功能吗?

        也是可以的,同理交换该节点与其后的节点数据,删除其后节点的数据即可,但是当该节点为尾节点时,就不能实现该功能了

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

	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		// 找到pos的前一个位置
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
		//pos = NULL;
	}
}

SLTInsertAfter 后插入

  • 创建新节点
  • 插入赋值操作,注意交换顺序,避免节点自我链接导致死循环
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

SLTEraseAfter 后删除

  • 断言该节点的下一节点是否为空
  • 如果使用pos->next = pos->next->next方法需要提前记录要删除的那个节点,避免内存泄漏
// pos位置后面删除
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);

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

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

SLTDestroy 释放 

  • 节点释放后,结构体指针还是指向该节点的,但是节点释放后,该空间已归还操作系统,里面的值是随机的,再次使用该节点赋值会造成野指针情况

注意,下面这种形式代码是错误的,是对指针的认识不正确造成的,可以画图理解

void SLTDestroy(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur)
	{
		SLTNode* tmp = cur;
		free(cur);
		//cur = cur->next;  野指针
		cur = tmp->next; //同样错误,没区别
	}
}

正确代码:

void SLTDestroy(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* tmp = cur->next;
		free(cur);
		//cur = cur->next;  野指针
		cur = tmp; //同样错误,没区别
	}
    //phead = NULL;  这里置空是没用的,改变不了phead的值,可以让使用者外部置空
    *pphead = NULL;
}

三、完整链表代码

1.SList.h 

#pragma once

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

typedef int SLTDataType;

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

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

void SLTPrint(SLTNode* phead);
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);

void SLTPopBack(SLTNode** pphead);
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);
//
void SLTEraseAfter(SLTNode* pos);

void SLTDestroy(SLTNode** phead);

2.SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"

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

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

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

	SLTNode* newnode = BuySLTNode(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)
{
	assert(pphead);

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


void SLTPopBack(SLTNode** pphead)
{
	// 暴力检查
	assert(pphead);
	assert(*pphead);

	// 温柔的检查
	//if (*pphead == NULL)
	//	return;

	// 1、只有一个节点
	// 2、多个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		// 找尾
		//SLTNode* prev = NULL;
		//SLTNode* tail = *pphead;
		//while (tail->next != NULL)
		//{
		//	prev = tail;
		//	tail = tail->next;
		//}

		//free(tail);
		//tail = NULL;

		//prev->next = NULL;

		SLTNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}

		free(tail->next);
		tail->next = NULL;
	}
}

void SLTPopFront(SLTNode** pphead)
{
	// 暴力检查
	assert(pphead);
	assert(*pphead);

	// 温柔的检查
	//if (*pphead == NULL)
	//	return;

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

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* cur = phead;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}

		cur = cur->next;
	}

	return NULL;
}

// pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	assert(pphead);

	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		// 找到pos的前一个位置
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

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

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

	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		// 找到pos的前一个位置
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
		//pos = NULL;
	}
}

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

// pos位置后面删除
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);

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

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

//释放
void SLTDestroy(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* tmp = cur->next;
		free(cur);
		//cur = cur->next;  野指针
		cur = tmp; //同样错误,没区别
	}
    //phead = NULL;  这里置空是没用的,改变不了phead的值,可以让使用者外部置空
    *pphead = NULL;
}

3. Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"

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

	SLTPrint(plist);
}

void TestSList2()
{
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPrint(plist);
}

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

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);
}

int main()
{
	TestSList3();

	return 0;
}

//void Func(int y)
//{
//	y = 1;
//}

//void Func(int* p)
//{
//	*p = 1;
//}
//
//int main()
//{
//	int x = 0;
//	Func(&x);
//
//	return 0;
//}

//void Func(int* ptr)
//{
//	ptr = (int*)malloc(sizeof(int));
//}
//
//int main()
//{
//	int* px = NULL;
//	Func(px);
//  free(px);
// 
//
//	return 0;
//}

//void Func(int** pptr)
//{
//	*pptr = (int*)malloc(sizeof(int));
//}
//
//int main()
//{
//	int* px = NULL;
//	Func(&px);
//
//	free(px);
//
//	return 0;
//}

总结

        熟记其中各函数定义时的小细节以防出错,此外还可自行编写菜单功能方便使用者使用。

以上就是今天要讲的内容,本文仅仅简单介绍了单链表如何编写以及其中的小小细节,要掌握单链表还要多多理解记忆与练习。

        下一节,小帅会带大家一起练习单链表的各种经典习题。最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值