数据结构初阶之单链表

1.单链表的实现

我们不再过的的废话,直接来看关于单链表的一些功能函数:

c 复制代码
void SLTPrint(SLTNode* phead);

void SLTPushBack(SLTNode** phead, SLTDataType x);

void SLTPushFront(SLTNode** phead, SLTDataType x);

void STLPopBack(SLTNode** phead);

void SLTPopFront(SLTNode** phead);

SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x);

void SLTInsertAfter(SLTNode* pos, SLTDataType x);

void SLTErase(SLTNode** phead, SLTNode* pos);

void SLTEraseAfter(SLTNode* pos);

void SListDesTroy(SLTNode** phead);

这些是涉及我们这次要实现的函数功能,我们来一一解决它们

不过在此之前,我们首先看一下关于单链表的实现:

1.1实现

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

上面的是前置条件,在链表中我们存储data,并存储指向下一个节点的指针,

可视化理解:

一个节点在内存中的样子:

c 复制代码
┌───────────────────────┐
│        Node           │
├───────────┬───────────┤
│   data    │   next    │
│  (值)     │  (指针)    │
│   e.g. 10 │ 0x123456  │
└───────────┴───────────┘

多个节点连接成链表:

c 复制代码
节点1 (地址: 0x1000)       节点2 (地址: 0x2000)       节点3 (地址: 0x3000)
┌───────────┬───────────┐  ┌───────────┬───────────┐  ┌───────────┬───────────┐
│   data:1  │ next:0x2000│→ │   data:2  │ next:0x3000│→ │   data:3  │ next:NULL │
└───────────┴───────────┘  └───────────┴───────────┘  └───────────┴───────────┘
c 复制代码
void test1()
{
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;

	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;

	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;

	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;
	SLTNode* plist = node1;
	SLTPrint(plist);
}

链表节点存储两个关键信息:

  • 数据值(data):节点的实际内容

  • 指向下一个节点的指针(next):将节点连接成链的关键

2单链表函数

2.1单链表打印

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

我们首先定义pcur指向头节点,while循环,依次打印节点中的data值即可

2.2 单链表的尾插

c 复制代码
void SLTPushBack(SLTNode** phead, SLTDataType x)
{
	assert(phead);
	SLTNode* newnode = SLTBuyNode(x);
	if (*phead == NULL)
	{
		*phead = newnode;
	}
	else
	{
		SLTNode* ptail = *phead;
		while (ptail->next)
		{
			ptail = ptail->next;
		}
		ptail->next = newnode;
	}
}

首先,我们传过来的二级指针不能为空,但是一级指针是可以的,毕竟传过来的链表为空,那我尾插也没什么问题,我们定义新节点为newnode,那么可能会有人说SLTBuyNode(x)是什么玩意,这个其实是因为在函数功能中我们总会用到申请空间的一个功能,所以我们干脆再写一个函数完成这个功能,如下:

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;
}

好了,继续我们的思路,如果我们传过来的链表为空,那么走if函数中,直接把新申请的给我们的空链表就可以了,如果不为空链表,那么定义个ptail节点,进入while循环中,直到找到最后一个节点,然后把ptail->next = newnode即可

其实这里还有一个问题,为什么这里要传二级指针呢?

2.2.1为什么要传二级指针?

答案只有一个!那就是如果传一级指针,那么我们的形参并不会影响实参,这种传递只是传值,它们之间互不影响罢了,可能到这里还是不明白为什么,没关系,我们这里来详细探讨下:

场景一:链表为空时插入第一个节点

c 复制代码
SLTNode* plist = NULL;  // plist本身是个指针,现在是NULL
SLTPushBack(plist, 1);   // 如果传plist,只是传值

如果SLTPushBack参数是SLTNode* phead

  • phead是plist的副本

  • 你在函数里phead = newnode,只是改了副本

  • 函数返回后,外面的plist还是NULL 😢

    场景二:用二级指针

c 复制代码
void SLTPushBack(SLTNode** pphead, SLTDataType x) {
    // pphead 是 &plist,*pphead 就是 plist
    // 修改 *pphead = newnode,就是修改了 plist 本身
}

"第一个节点"和"指向第一个节点的指针"是什么?

c 复制代码
实际内存结构:
┌─────┬─────────────┐   ┌─────┬─────────────┐
│ data│ next        │   │ data│ next        │
│  1  │ 0x1000─────▶│   │  2  │ 0x2000─────▶│
└─────┴─────────────┘   └─────┴─────────────┘
地址:0x1000               地址:0x2000

plist(指针变量):
┌─────────────┐
│ 值:0x1000  │ ← 指向第一个节点
└─────────────┘
地址:0x3000(假设)
  • 第一个节点:内存地址为0x1000的那个结构体

  • 指向第一个节点的指针:变量plist,它存的值是0x1000

  • 指向第一个节点的指针的地址:&plist,值是0x3000

    形象比喻:

  • 一级指针 = 你家的门牌号(知道房子在哪)

  • 二级指针 = 知道你家门牌号写在哪个本子上

  • 要换房子(空链表插入)时,得找到记门牌号的本子(二级指针),改上面的门牌号

简单记忆:

  • 如果函数要修改链表头指针(空链表插入、删除第一个节点等),传二级指针&plist

  • 如果函数只是遍历/读取,传一级指针plist

为什么难理解?

因为指针就像"遥控器",你手里拿着遥控器(一级指针)。如果你想在另一个房间(函数里)换台(修改链表头),得有人把遥控器给你。但如果只是告诉你现在播什么节目(读取),就不需要给遥控器。

如果有些人到这里还是不太理解,那我们不妨再做个对照实验:

场景1:修改普通变量

c 复制代码
void changeNumber(int x) {
    x = 100;  // 修改副本
}

int main() {
    int a = 10;
    changeNumber(a);  // 传值
    printf("%d", a);  // 输出:10,a没变!
}

这里大家都明白:传值只是传了副本

场景2:修改指针指向的值

c 复制代码
void changeValue(int* p) {
    *p = 100;  // 通过指针修改原值
}

int main() {
    int a = 10;
    changeValue(&a);  // 传地址
    printf("%d", a);  // 输出:100,a变了!
}

这里也简单:用一级指针修改指针指向的数据。

场景3:修改指针本身(这才是你的困惑)

c 复制代码
// 这个函数想改变指针本身(让它指向新地址)
void changePointer(int* p) {
    p = malloc(sizeof(int));  // 这只能改变局部副本p
    *p = 200;
}

int main() {
    int* ptr = NULL;  // ptr是一个指针
    changePointer(ptr);  // 想改变ptr,让它指向新内存
    // 问题:ptr仍然是NULL!
}

关键来了:ptr本身是一个指针变量,它存着一个地址。当我们说"想修改ptr",意思是修改ptr这个变量里存的地址值。

  • ptr = 0x1000 → 改成 ptr = 0x2000

  • 但ptr作为参数传给函数时,函数得到的是ptr值的副本

  • 就像int a = 10传值给函数一样

    可视化内存变化:

c 复制代码
调用前:
plist: [NULL]  地址0x1000

调用SLTPushBack(plist, 1):
在栈上创建局部变量phead: [NULL]  地址0x2000 (是plist的副本)

在函数里:phead = newnode
phead: [0x3000]  地址0x2000 (修改了局部变量)

函数返回:phead销毁
plist: [NULL]  地址0x1000 (原指针没变!)

总结成一句话:

  • 你想修改谁的"值",就要传谁的"地址"。

  • 想修改int a的值 → 传&a (一级指针)

  • 想修改int* ptr的值(这个值是个地址) → 传&ptr (二级指针)

在我们的例子中:

  • plist是一个SLTNode*类型的一级指针

  • 你想修改plist这个指针变量的值(从NULL改成新节点地址)

  • 所以要传plist的地址,即&plist,这就是二级指针

再打个比方:

  • plist = 一个信封,里面写着一个地址(现在是空的)

  • SLTPushBack(plist, x) = 我给你这个信封的复印件

  • 你在复印件上写新地址,不影响我的原信封

  • SLTPushBack(&plist, x) = 我给你我放信封的抽屉位置

  • 你打开抽屉,直接在我的原信封上写新地址

    现在大家明白了嘛?

2.3单链表的头插

c 复制代码
void SLTPushFront(SLTNode** phead, SLTDataType x)
{
	assert(phead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *phead;
	*phead = newnode;
}

首先还是二级指针不能为空,我让我定义的新节点指向头节点,再让*phead指向新节点,这样新节点就为头节点了

2.4单链表的尾删

c 复制代码
void STLPopBack(SLTNode** phead)
{
	assert(phead && *phead);
	//链表中只有一个节点
	if ((*phead)->next == NULL)
	{
		free((*phead));
		*phead = NULL;
	}
	else
	{
		SLTNode* prev = *phead;
		SLTNode* ptail = *phead;
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		free(ptail);
		prev->next = NULL;
		ptail = NULL;
	}
}

我们想要尾删是不是需要找到最后一个节点呢?我们这里定义两个指针,当while循环,ptail->next结束后就找到了尾节点,但是我们还需要找到尾节点的前一个节点,为什么呢?如果我们找到尾节点直接释放掉的话,那我们尾节点的前一个节点指向哪里呢?对吧,所以我们需要找到尾节点的前一个节点,当找到这两个节点时,我把尾节点释放掉,前一个节点的next指向空指针,再把尾节点置为空即可。

2.5单链表的头删

c 复制代码
void SLTPopFront(SLTNode** phead)
{
	assert(phead && *phead);
	SLTNode* next = (*phead)->next;
	free(*phead);
	*phead = next;

}

这个其实很简单,首先定义个节点指向头节点的下一个节点,然后释放掉头节点,使下一个节点成为头节点即可。

2.6单链表的查找

c 复制代码
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

这个没啥好讲,很简单,过。

2.7单链表关于在指定位置之前插入数据

c 复制代码
void SLTInsert(SLTNode** phead, SLTNode* pos, SLTDataType x)
{
	assert(phead && *phead);
	assert(pos);
	if (*phead == pos)
	{
		SLTPushFront(phead,x);
	}
	else
	{
		SLTNode* newnode = SLTBuyNode(x);
		SLTNode* prev = *phead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		newnode->next = pos;
		prev->next = newnode;
	}
}

其实这里就是把前面综合一点而已,既然我们想要再pos节点之前插入数据,那么就先找到pos的前一个节点,然后把新节点的next指向pos,pos的前一个节点的next指向新节点即可,这样不就插入进去了嘛。

但是当我们注意,*phead == pos时这样的代码是行不通的,所以我们需要单独来讨论一下即可。

2.8单链表关于在指定位置之后插入数据

c 复制代码
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

这个比刚才的更简单,因为在后,所以直接省一个参数,我们把新节点next指向pos->next即可,pos->next = newnode就可以啦,如图:

2.9单链表关于删除pos节点

c 复制代码
void SLTErase(SLTNode** phead, SLTNode* pos)
{
	assert(phead && *phead);
	assert(pos);
	if (pos == *phead)
	{
		//头删
		SLTNode* next = (*phead)->next;
		free(pos);
		(*phead) = next;
	}
	else
	{
		SLTNode* pcur = *phead;
		while (pcur->next != pos)
		{
			pcur = pcur->next;
		}
		pcur->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

后面的跟前面都大同小异,就不说了。

2.10单链表关于删除pos之后的节点

c 复制代码
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos&&pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

2.11单链表关于销毁单链表

c 复制代码
void SListDesTroy(SLTNode** phead)
{
	assert(phead && *phead);
	SLTNode* pcur = *phead;
	while (pcur)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;

	}
	*phead = NULL;
}
相关推荐
报错小能手2 小时前
数据结构 AVL树讲解
数据结构·算法
报错小能手2 小时前
数据结构 布隆过滤器
数据结构
charliejohn2 小时前
计算机考研 408 数据结构 时间复杂度分析 计算题例题及解析
数据结构·考研·算法
小尧嵌入式2 小时前
C++选择排序插入排序希尔排序快排归并排及大小根堆实现优先级队列
数据结构·c++·windows·算法·排序算法
吴声子夜歌2 小时前
数据结构——二叉树
数据结构
天赐学c语言3 小时前
12.29 - 字符串相加 && vector和map的区别
数据结构·c++·算法·leecode
CodeByV3 小时前
【算法题】位运算
数据结构·算法
黎雁·泠崖3 小时前
C 语言文件操作高阶:读取结束判定 + 缓冲区原理 + 常见错误
c语言·开发语言·缓存