(数据结构)线性表(中):SLIst单链表

链表

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

逻辑结构线性,物理结构(绝大多数情况下)非线性。
结构 :链表是由结点组成的,而结点=存储数据+指向下一数据的指针

单链表的构建

  • 思考的角度:前提条件、基本实现方法、需要特殊考虑的情况、什么代码可以复用
  • 测试时调用函数关注一下参数有没有传错!

结点

c 复制代码
typedef int SLTDataType;
typedef struct SListNode {
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

注意:C语言是向上编译的,所以在取别名SLTNode前就使用这个别名会导致报错,因为使用之前并没有取好别名!所以定义next指针还是要写struct SListNode* next

遍历与打印单链表

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

初始化新结点

c 复制代码
//向操作系统申请一个新结点并初始化
SLTNode* SLTBuyNode(SLTDataType x) {
	//创建新结点
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {
		perror("Malloc fail!");
		exit(1);
	}
	//将数据存入新节点
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

尾插

注意要点:

  1. 当头节点是空指针时,需要修改指针本身的值 ,使其指向创建的新结点,所以注意要进行传址传参,即参数是二级指针SLTNode** pphead,同时因为代码中要用到*pphead,注意到NULL不可以解引用的原则,增加断言assert(pphead)
    3. 要对尾结点进行操作,需要找到的是ptail->next的这个点,而不是ptail=NULL的点,这个点已经越过了。
c 复制代码
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL) {
		*pphead = newnode;
	}
	else {
		SLTNode* ptail = *pphead;
		while (ptail->next) {//等价于ptail->next!=NULL,即遍历到尾节点
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

为什么我们遍历的时候,总是重新定义一个新指针SLTNode* ptail = *pphead; ``SLTNode* pcur = phead来进行遍历?

因为我们要保留好头节点的地址,防止后续还要使用到前面的数据时出现困难。

头插

看参数传的是一级还是二级指针,就思考方法是否要改变指针本身的指向,即其本身的值

注意:

  1. phead永远指向头结点
  2. 初始链表为空时让phead直接指向新结点
    在单链表中,头插的时间复杂度仅为O(1)
c 复制代码
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	SLTNode* newnode=SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

尾删

  1. 当链表为空时(*pphead=NULL),删除操作就没有意义,所以把这个条件加入assert中判断,同时pphead仍然不能为NULL。
  2. 当链表中只有一个结点时,即删除的是头节点时,要改变phead的指向,所以函数参数仍然是二级指针pphead。
    ->操作符优先级高于*操作符,所以条件注意写成(*pphead)->next==NULL
  3. 当链表中有多个结点时,首先要找到尾结点的前一个结点指针prev,将prev->next=NULL(直接释放尾节点,prev->next会成为野指针)才能释放尾节点。
  4. 删除操作需要注意的点:关注野指针,free+置空
c 复制代码
//尾删
void SLTPopBack(SLTNode** pphead) {
	assert(pphead && *pphead);
	//只有一个结点的情况
	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;
	}
	else {
		SLTNode* prev = NULL;
		SLTNode* ptail = *pphead;
		while (ptail->next) {
			prev = ptail;
			ptail = ptail->next;
		}
		prev->next = NULL;
		free(ptail);
		ptail = NULL;//free完任何指针以后都要置空!
	}
}

头删

  1. 每一个结点一一相连,所以头结点不能直接释放掉,而是要先找到第二个结点存起来,再释放掉第一个结点。
  2. 检验当链表中只有一个结点时,和有多个结点的方法可以通用。
c 复制代码
//头删
void SLTPopFront(SLTNode** pphead) {
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

查找

c 复制代码
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {
	SLTNode* pcur = phead;
	//注意这里是遍历链表,而不是找到尾结点!
	//所以条件是pcur而不是pcur->next!!!
	while (pcur) {
		if (pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}
	//未找到
	return NULL;
}

在指定位置之前插入数据

  1. 链表为空或pos为NULL时功能没有意义
  2. 链表的位置用结点的指针表示
  3. 分只有一个结点和有多个结点两种情况
  4. 有多个结点时,先找到pos的前一个结点,用prev存储
c 复制代码
//在pos前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
	//传入的结点位置为空是没有意义的
	assert(pphead&&pos);
	SLTNode* newnode = SLTBuyNode(x);
	if (pos == *pphead) {
		//pos是头结点时,等价于头插
		SLTPushFront(pphead, x);
	}
	else{
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

在指定位置之后插入数据

先让newnode接上pos->next,再让pos->next指向newnode,直接调换顺序是不行的

c 复制代码
//在pos后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
	//注意这两步不能直接调换!
}

删除指定位置的结点

  • 已知指定位置结点时,什么时候要传入头结点?
    当需要找指定位置之前的结点时要传入,因为单链表只能从前往后找,而无法从后往前回找。
  • 可复用的代码尽量复用
c 复制代码
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos) {
	//pphead为空、pos为空都没意义(不用特地强调链表为空,因为pos不为空显然链表就不会为空了)
	assert(pphead && pos);
	//如果删除的是头结点要单独讨论,否则下面代码会出错
	if (pos == *pphead) {
		SLTPopFront(pphead);
	}
	else {
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

删除指定位置之后的结点

  1. 前提条件:pos之后的结点不能为空(删除一个空结点没有意义)
  2. 不需要传头结点了,因为这里只涉及pos及它之后的结点操作
c 复制代码
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos) {
	//pos之后的结点若为空,这个功能也没有意义
	assert(pos && pos->next);
	//先储存要删除的pos的下一个结点,否则一会pos改变指向了找不到
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

销毁链表

c 复制代码
//销毁链表
void SListDestroy(SLTNode** pphead) {
	assert(pphead);
	SLTNode* pcur = *pphead;
	while (pcur) {
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;//这是让函数外的头指针本身置空
}

完整代码

SList.h

c 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

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

}SLTNode;

//遍历并打印链表
void SLTPrint(SLTNode* phead);
//向操作系统申请一个新结点并初始化
SLTNode* SLTBuyNode(SLTDataType x);
//尾插
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 SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDestroy(SLTNode** pphead);

SList.c

c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"

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

//向操作系统申请一个新结点并初始化
SLTNode* SLTBuyNode(SLTDataType x) {
	//创建新结点
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {
		perror("Malloc fail!");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL) {
		*pphead = newnode;
	}
	else {
		SLTNode* ptail = *pphead;
		while (ptail->next) {//等价于ptail->next!=NULL,即遍历到尾节点
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	SLTNode* newnode=SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

//尾删
void SLTPopBack(SLTNode** pphead) {
	assert(pphead && *pphead);
	//只有一个结点的情况
	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;
	}
	else {
		SLTNode* prev = NULL;
		SLTNode* ptail = *pphead;
		while (ptail->next) {
			prev = ptail;
			ptail = ptail->next;
		}
		prev->next = NULL;
		free(ptail);
		ptail = NULL;//free完任何指针以后都要置空!
	}
}

//头删
void SLTPopFront(SLTNode** pphead) {
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {
	SLTNode* pcur = phead;
	//注意这里是遍历链表,而不是找到尾结点!
	//所以条件是pcur而不是pcur->next!!!
	while (pcur) {
		if (pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}
	//未找到
	return NULL;
}
//在pos前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
	//传入的结点位置为空是没有意义的
	assert(pphead&&pos);
	SLTNode* newnode = SLTBuyNode(x);
	if (pos == *pphead) {
		//pos是头结点时,等价于头插
		SLTPushFront(pphead, x);
	}
	else{
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}
//在pos后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
	//注意这两步不能直接调换!
}
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos) {
	//pphead为空、pos为空都没意义(不用特地强调链表为空,因为pos不为空显然链表就不会为空了)
	assert(pphead && pos);
	//如果删除的是头结点要单独讨论,否则下面代码会出错
	if (pos == *pphead) {
		SLTPopFront(pphead);
	}
	else {
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos) {
	//pos之后的结点若为空,这个功能也没有意义
	assert(pos && pos->next);
	//先储存要删除的pos的下一个结点,否则一会pos改变指向了找不到
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

//销毁链表
void SListDestroy(SLTNode** pphead) {
	assert(pphead);
	SLTNode* pcur = *pphead;
	while (pcur) {
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;//这是让函数外的头指针本身置空
}

test.c

c 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"

void test01() {
	SLTNode* plist=NULL;//创建头节点->创建空链表
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
}

void test02() {
	SLTNode* plist = NULL;//创建头节点->创建空链表
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPrint(plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPrint(plist);
}
void test03() {
	SLTNode* plist = NULL;
	SLTPushFront(&plist, 1);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 4);
	SLTPrint(plist);
	//SLTNode* ret=SLTFind(plist, 3);
	SLTNode* ret = SLTFind(plist, 333);
	if (ret!=NULL) {
		printf("找到了\n");
	}
	else {
		printf("没找到\n");
	}
}

void test04() {
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
	SLTNode* pos1 = SLTFind(plist, 2);
	SLTInsert(&plist, pos1, 100);
	SLTPrint(plist); 
	SLTNode* pos2 = SLTFind(plist, 4);
	SLTInsertAfter(pos2, 200);
	SLTPrint(plist);
	pos1 = SLTFind(plist, 2);
	SLTErase(&plist, pos1);
	SLTPrint(plist);
	//C 语言所有参数都是"按值传递"
	//	你传进去的是指针变量的值(也就是那块内存的地址),函数内部拿到的只是一个 副本。
	//	因此在函数里:
	//	c
	//	复制
	//	free(pos);   // 释放的是 p 指向的那块内存
	//  pos = NULL;  // 只把副本改成 NULL,外面的原指针依旧保留原来的地址值
	//结果:
	//	• 内存确实被释放了;
	//	• 外部变量pos1仍保存着 已释放的地址(野指针);
	//	• 外部再解引用就是 未定义行为(可能崩溃、可能脏数据)。
	pos2 = SLTFind(plist, 4);
	SLTEraseAfter(pos2);
	SLTPrint(plist);
}

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

总结

在完整实现顺序表和链表之后,我们可以对他们进行对比分析。

  • 链表在头部(O(1))和中间的插入删除时间复杂度小于顺序表

  • 链表不需要进行增容,开一个用一个,没有额外的空间浪费

相关推荐
一棵开花的树,枝芽无限靠近你1 小时前
数据结构之克鲁斯卡尔算法
数据结构·算法·c
hope_wisdom5 小时前
C/C++数据结构之单向链表
c语言·数据结构·c++·链表·单向链表
clock的时钟8 小时前
数据结构-线性表顺序表示
数据结构·算法
nako_sayuri9 小时前
二分查找:区间内查询数字的频率
数据结构·算法
艾莉丝努力练剑9 小时前
【LeetCode&数据结构】栈的应用——有效的括号问题详解
c语言·开发语言·数据结构·学习·链表
The Chosen One9859 小时前
C++的“链”珠妙笔:list的编程艺术
开发语言·数据结构·c++·list
Code Warrior10 小时前
【每日算法】专题十七_多源 BFS
数据结构·c++·算法·宽度优先
爱装代码的小瓶子10 小时前
数据结构之栈
c语言·开发语言·数据结构·算法
Gu_shiwww10 小时前
C语言基础7——两种简单排序算法和二维数组
c语言·数据结构·算法·小白初步