数据结构003 - 线性表(单链表)

1. 链表的概念及结构

1.1 链表的概念

链表是一种 物理存储结构上非连续、非顺序的存储结构,数据元素的 逻辑顺序 是通过链表中的指针链接次序实现的 。
在这里插入图片描述

下图为可能的物理分布
在这里插入图片描述

1.2 链表的优缺点

优点:
1、按需申请空间,不使用就释放空间(更合理地使用空间)。
2、在链表头部/中间插入/删除数据,不需要挪动数据。
3、不存在空间浪费。
缺点:
1、每一个数据,都要储存一个指针链接后面的数据节点。
2、不支持随机访问(用下标直接访问第 i 个)。

1.3 链表的结构分类

在实际中,链表的结构非常多样,分类有如下几种:
1、单向链表双向链表
链表1
2、不带头链表带头链表(此处 “头” 也称为 “哨兵”)
链表2
3、非循环链表循环链表
链表3
将以上结构排列组合起来,可得有八种结构的链表。在实际中,最常用的两种结构是 无头单向非循环链表带头双向循环链表
在这里插入图片描述
(1)无头单向非循环链表 :结构简单,一般不会单独用于存储数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等。
(2)带头双向循环链表 :结构最复杂,一般用于单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这种结构虽然结构复杂,但是在使用代码实现以后,会发现结构会带来很多优势,实现反而简单了。

2. 单链表的实现

代码的实现将放在三个文件中,分别是 SList.h(用于声明)、SList.c(用于定义)、Test.c(用于测试)

SList.h 文件

#pragma once

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

typedef int SLTDataType;

// 单链表的结构
typedef struct SListNode{
	SLTDataType data;       // 数据域,储存数据
	struct SListNode* next; // 指针域,储存下一个节点的地址
}SListNode;


// 申请一个新节点
SListNode* BuySListNode(SLTDataType x);

// 打印单链表
void SListPrint(SListNode* phead);

// 尾插
void SListPushBack(SListNode* phead, SLTDataType x);

// 头插
void SListPushFront(SListNode** pphead, SLTDataType x);

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

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

// 查找
SListNode* SListFind(SListNode* phead, SLTDataType x);

// 在 pos 位置之前插入一个节点 
void SListInsertBefore(SListNode** pphead, SListNode* pos, SLTDataType x);

// 在 pos 位置之后插入一个节点 
void SListInsertAfter(SListNode* pos, SLTDataType x);

// 删除 pos 位置的节点
void SListErase(SListNode** pphead, SListNode* pos);

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

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

2.1 申请一个新节点

SList.c 文件

#include "SList.h"

// 申请一个新节点
SListNode* BuySListNode(SLTDataType x){
	SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
	// malloc 后需要检查是否申请成功,因此要判断 newnode 是否为空
	if (newnode == NULL){
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;    // x 的值储存在 newnode 数据域中
	newnode->next = NULL; // newnode 的指针域置空

	return newnode;
}

2.2 打印单链表

SList.c 文件

#include "SList.h"

// 打印单链表
void SListPrint(SListNode* phead){
	SListNode* cur = phead;

	while (cur != NULL){
		printf("%d->", cur->data);  // 打印链表数据
		cur = cur->next; // 让 cur 指向下一个数据
	} // 当 cur 为空时,链表打印结束
	printf("NULL\n");
}

2.3 尾插

思路:

(1)如果链表本身是空链表,则把新申请的节点分配给 phead。需要注意的是:形参是实参的临时拷贝,形参的改变不影响实参。这里要改变实参 plist,形参 phead 是实参 plist 的一份临时拷贝,形参 phead 的改变不影响实参 plist,所以要传 plist 的地址,而 plist 本身就是指针,所以要传指针的地址,那么就要用二级指针来接收。

(2)如果链表不是空链表,此时就需要创建一个指针变量 tail 来遍历链表,找到链表的最后一个节点,然后把新创建的节点插入进去。

思路

SList.c 文件

#include "SList.h"

// 尾插
void SListPushBack(SListNode** pphead, SLTDataType x){
	assert(pphead);
    
    // 申请一个新节点
	SListNode* newnode = BuySListNode(x);

	if (*pphead == NULL){
		*pphead = newnode;
	}
	else{
		// 寻找尾节点
		SListNode* tail = *pphead;
		while (tail->next != NULL){
			tail = tail->next;
		}
		// 找到尾节点,将尾节点的指针域指向插入的新节点的地址
		tail->next = newnode;
	}
}

Test.c 文件

#include "SList.h"

void TestSList1(){
	// 空链表
	SListNode* plist = NULL;
	// 尾插
	for (int i = 0; i < 4; i++){
		SListPushBack(&plist, i);
	}
	// 打印单链表
	SListPrint(plist);
}

int main(){
	TestSList1();

	return 0;
}

运行结果:

运行结果

2.4 头插

思路: 头插相对于尾插简单一点,只需要把新创建的节点放入链表头部,需要注意的是头插和尾插一样,需要传二级指针。

思路

SList.c 文件

#include "SList.h"

// 头插
void SListPushFront(SListNode** pphead, SLTDataType x){
	assert(pphead);
    
    // 申请一个新节点
	SListNode* newnode = BuySListNode(x);

	newnode->next = *pphead; // newnode 的指针域指向链表的数据域(链表的头)
	*pphead = newnode; // 让 newnode 作为新链表的头
}

Test.c 文件

#include "SList.h"

void TestSList1(){
	// 空链表
	SListNode* plist = NULL;
	// 尾插
	for (int i = 0; i < 4; i++){
		SListPushBack(&plist, i);
	}
	// 打印单链表
	SListPrint(plist);

	// 头插
	for (int i = 1; i < 4; i++){
		SListPushFront(&plist, i * 10);
	}
	// 打印单链表
	SListPrint(plist);
}


int main(){
	TestSList1();

	return 0;
}

运行结果:

运行结果

2.5 尾删

思路: 寻找最后一个节点,定义一个指针变量 tail 遍历链表,当 tail->next == NULL 时就找到了尾节点。但是当 free 最后一个节点时,无法找到倒数第二个节点,而尾删需要将倒数第二个节点的 next 置为空,所以还需要创建一个指针变量 prev,尾删后将 prev->next 置为空。

另外,尾删还需要分链表为空无节点、只有一个节点、多个节点三种情况判断。

思路

SList.c 文件

#include "SList.h"

// 尾删
void SListPopBack(SListNode** pphead){
	assert(pphead);
    
    // 链表为空时,粗暴的处理
	// assert(*pphead != NULL);

	// 链表为空时,温柔的处理
	if (*pphead == NULL){
		return;
	}
	// 只有一个节点
	else if ((*pphead)->next == NULL){
		free(*pphead);
		*pphead = NULL;
	}
	// 有多个节点
	else{
		// 先找到尾节点,再记录尾节点的前一个节点
		SListNode* tail = *pphead;
		SListNode* prev = NULL;
		while (tail->next != NULL){
			prev = tail;
			tail = tail->next;
		}
		// 找到尾节点,释放并置空;
		free(tail);
		tail = NULL;
		// 此时尾节点的前一个节点(prev)还保存着 tail 节点的数据的地址,要将它置空;
		prev->next = NULL;
	}
}

Test.c 文件

#include "SList.h"

void TestSList1(){
	// 空链表
	SListNode* plist = NULL;
	// 尾插
	for (int i = 0; i < 4; i++){
		SListPushBack(&plist, i);
	}
	// 打印单链表
	SListPrint(plist);

	// 尾删
	SListPopBack(&plist);
	SListPopBack(&plist);
	// 打印单链表
	SListPrint(plist);
}


int main(){
	TestSList1();

	return 0;
}

运行结果:

运行结果

2.6 头删

思路: 当链表为空时,不用删除直接返回空链表。当链表不为空时,可以创建一个指针变量 next 来保存第二个节点的地址,即第一个节点的 next,然后 free 第一个节点,将 next 赋值给 *pphead。

SList.c 文件

#include "SList.h"

// 头删
void SListPopFront(SListNode** pphead){
	assert(pphead);
    
    if (*pphead == NULL){
		return;
	}
	else{
		// 记录头节点储存的下一个节点的地址
		SListNode* next = (*pphead)->next;
		free(*pphead); // 释放头节点
		*pphead = next; // 头节点的下一个节点作新的头节点
	}
}

Test.c 文件

#include "SList.h"

void TestSList1(){
	// 空链表
	SListNode* plist = NULL;
	// 尾插
	for (int i = 0; i < 4; i++){
		SListPushBack(&plist, i);
	}
	// 打印单链表
	SListPrint(plist);

	// 头删
	SListPopFront(&plist);
	SListPopFront(&plist);
	// 打印单链表
	SListPrint(plist);
}


int main(){
	TestSList1();

	return 0;
}

运行结果:

运行结果

2.7 查找、修改

思路: 遍历链表。

SList.c 文件

#include "SList.h"

// 查找
SListNode* SListFind(SListNode* phead, SLTDataType x){
	SListNode* cur = phead;
	while (cur != NULL){
		if (cur->data == x){
			return cur;
		}
		else{
			cur = cur->next;
		}
	}
	return NULL;
}

Test.c 文件

#include "SList.h"

void TestSList1(){
	// 空链表
	SListNode* plist = NULL;
	// 尾插
	SListPushBack(&plist, 1);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 3);
	SListPushBack(&plist, 4);
	SListPushBack(&plist, 2);
	SListPushBack(&plist, 5);
	SListPushBack(&plist, 2);
	// 打印单链表
	SListPrint(plist);

	// 查找
	SListNode* pos = SListFind(plist, 2);
	int i = 1;
	while (pos){
		printf("找到了,第 %d 个 pos 节点:%p->%d\n", i++, pos, pos->data);
		pos = SListFind(pos->next, 2);
	}
	
	// 修改
	pos = SListFind(plist, 3);
	if (pos){
		pos->data = 30;
	}
	
	// 打印单链表
	SListPrint(plist);
}


int main(){
	TestSList1();

	return 0;
}

运行结果:

运行结果

2.8 在 pos 位置之前插入一个节点

思路:

(1)pos 不在头节点 。pos 是节点的地址,通过 SListFind 函数找到 pos。同时还需要找到 pos 前一个节点的地址,只有这样才能将链表的节点依次链接起来。可以创建一个指针变量 posPrev,然后遍历链表,当 posPrev->next==pos 时,posPrev 就是 pos 前一个节点的地址,然后创建一个新的节点,实现节点的插入。

(2)pos 在头节点。此时相当于头插。

思路

SList.c 文件

#include "SList.h"

// 在 pos 位置之前插入一个节点 
void SListInsertBefore(SListNode** pphead, SListNode* pos, SLTDataType x){
	assert(pphead);
	assert(pos);
    
    // 申请一个新节点
	SListNode* newnode = BuySListNode(x);
	// 如果 pos 是头节点
	if (*pphead == pos){
		newnode->next = *pphead;
		*pphead = newnode;
		// pos 是第一个节点,相当于头插
		// SListPushFront(pphead, x);
	}
	// 如果 pos 不是头节点
	else{
		// 找到 pos 位置的前一个节点
		SListNode* posPrev = *pphead;
		while (posPrev->next != pos){
			posPrev = posPrev->next;
		}
		// 让新节点的指针域指向 pos,再让 pos 的前一个节点的指针域重新指向新节点的数据域地址,就插入到 pos 的前面了
		newnode->next = pos;
		posPrev->next = newnode;
	}
}

Test.c 文件

#include "SList.h"

void TestSList1(){
	// 空链表
	SListNode* plist = NULL;
	// 尾插
	for (int i = 0; i < 5; i++){
		SListPushBack(&plist, i);
	}
	// 打印单链表
	SListPrint(plist);

	SListNode* pos = SListFind(plist, 3);
	if (pos){
		SListInsertBefore(&plist, pos, 10);
	}
	// 打印单链表
	SListPrint(plist);
}


int main(){
	TestSList1();

	return 0;
}

运行结果:

运行结果

2.9 在 pos 位置之后插入一个节点

思路:更加推荐在 pos 位置之后插入的方式。 因为相较于在 pos 位置之前插入来说更加方便,不需要寻找 pos 位置的上一个节点。

思路

SList.c 文件

#include "SList.h"

// 在 pos 位置之后插入一个节点 
void SListInsertAfter(SListNode* pos, SLTDataType x){
	assert(pos);
    
    // 申请一个新节点
	SListNode* newnode = BuySListNode(x);
	// 让新节点的指针域指向 pos 位置的下一个节点的数据域地址
	newnode->next = pos->next;
	// pos 的指针域指向新节点的数据域地址
	pos->next = newnode;
}

Test.c 文件

#include "SList.h"

void TestSList1(){
	// 空链表
	SListNode* plist = NULL;
	// 尾插
	for (int i = 0; i < 5; i++){
		SListPushBack(&plist, i);
	}
	// 打印单链表
	SListPrint(plist);
	
	SListNode* pos = SListFind(plist, 3);
	if (pos){
		SListInsertAfter(pos, 10);
	}
	// 打印单链表
	SListPrint(plist);
}


int main(){
	TestSList1();

	return 0;
}

运行结果:

运行结果

2.10 删除 pos 位置的节点

思路: 当 pos 为第一个节点时,只需要调用头删。当 pos 不是第一个节点时,需要创建一个指针变量 posPrev 指向 pos 的前一个节点,然后将 posPrev->next 指向 pos 的下一个节点,再将 pos 节点 free 掉。

思路

SList.c 文件

#include "SList.h"

// 删除 pos 位置的节点
void SListErase(SListNode** pphead, SListNode* pos){
	assert(pphead);
	assert(pos);
    
    // 当 pos 为第一个节点时
	if (*pphead == pos){
		// 方法一:调用头删函数
		SListPopFront(pphead);

		// 方法二
		//pphead = pos->next;
		//free(pos);
	}
	else{
		// 找到 pos 位置的前一个位置(posPrev)
		SListNode* posPrev = *pphead;
		while (posPrev->next != pos){
			posPrev = posPrev->next;
		}
		// 让 pos 位置的前一个节点 posPrev 的指针域指向 pos 节点的指针域
		// 如果 pos 位置后还有节点,即指向了 pos 位置的下一个节点的数据域的地址
		posPrev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

Test.c 文件

#include "SList.h"

void TestSList1(){
	// 空链表
	SListNode* plist = NULL;
	// 尾插
	for (int i = 0; i < 4; i++){
		SListPushBack(&plist, i);
	}
	// 打印单链表
	SListPrint(plist);
	
	SListNode* pos = SListFind(plist, 2);
	if (pos){
		SListErase(&plist, pos);
	}
	// 打印单链表
	SListPrint(plist);
}


int main(){
	TestSList1();

	return 0;
}

运行结果:

运行结果

2.11 删除 pos 位置之后的节点

思路: 需要创建一个指针变量 next 指向 pos 的指针域,即指向了 pos 位置的下一个节点的数据域的地址。然后将 next->next(即保存的 pos 之后第二个节点的数据域的地址)赋值给 pos->next,最后将 next 变量 free 掉。

SList.c 文件

#include "SList.h"

// 删除 pos 位置之后的节点
void SListEraseAfter(SListNode* pos){
	assert(pos);
    assert(pos->next);
    
    SListNode* next = pos->next;
	if (next){
		pos->next = next->next;
		free(next);
		next = NULL;
	}
}

Test.c 文件

#include "SList.h"

void TestSList1(){
	// 空链表
	SListNode* plist = NULL;
	// 尾插
	for (int i = 0; i < 4; i++){
		SListPushBack(&plist, i);
	}
	// 打印单链表
	SListPrint(plist);
	
	SListNode* pos = SListFind(plist, 2);
	if (pos){
		SListEraseAfter(pos);
	}
	// 打印单链表
	SListPrint(plist);
}


int main(){
	TestSList1();

	return 0;
}

运行结果:

运行结果

2.12 销毁单链表

思路: 单链表的销毁并不能和顺序表一样直接释放,顺序表是开辟的连续的空间,链表不是。要将链表释放需要释放每一个结点;可以采用双指针。

思路

SList.c 文件

#include "SList.h"

// 销毁
void SListDestroy(SListNode** pphead){
	assert(pphead);

	SListNode* cur = *pphead;
	while (cur){
		SListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

Test.c 文件

#include "SList.h"

void TestSList1(){
	// 空链表
	SListNode* plist = NULL;
	// 尾插
	for (int i = 0; i < 4; i++){
		SListPushBack(&plist, i);
	}
	// 打印单链表
	SListPrint(plist);
	
	SListDestroy(&plist);
	// 打印单链表
	SListPrint(plist);
}


int main(){
	TestSList1();

	return 0;
}

运行结果:

运行结果

3. 单链表的适用场景

单链表结构,适合头插头删,不适合在尾部或者中间某个位置插入或者删除。如果要使用链表结构存储数据,使用 双向链表 更为合适。
单链表会作为复杂数据结构的子结构(图的邻接表、哈希桶)。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值