【初阶】数据结构 - 单向链表

【初阶】数据结构 - 算法效率 & 顺序表

上面的链接是顺序表的内容。这一篇博客中部份内容会使用到顺序表中提到的函数。若不清楚可以点击上面链接去查看

一、顺序表的缺陷

  1. 中间头部插入删除效率低 – 因为需要挪动数据 ⇒ {\Rightarrow} 时间复杂度 O(N)

  2. 空间不够需要扩容 – 扩容有一定的消耗,其次有空间浪费的可能

    例如: 我们100个空间不够了,藉由SLCheckCapacity()函数扩容后空间变为200,但我只插入5个数据,那剩下的95个空间就浪费了

链表可以解决顺序表的缺陷

在正式进入链表之前,让我们复习一下会使用到的概念

二、复习结构体相关概念

  1. 结构体中不能嵌套自身

    因为在编译阶段就必须要确定结构体的大小,如果在结构体中嵌套自身,就没办法知道结构体大小

    typedef struct Node {
        int data;
        struct Node next; // 错误,递归定义导致无法确定大小
    } Node;
    // 这段代码会导致无限递归
    
  2. 如果嵌套的是自身结构体的指针呢?

    此时就不会有问题发生,因为指针存的是地址,地址所占字节不是 4 就是 8 因此,结构体大小能确定

    #include <stdio.h>
    #include <stdlib.h>
             
    typedef struct Node {
        int data;
        struct Node* next; // 结构体指针,指向另一个 Node 结构
    } Node;
    

    在链表或是其他数据结构中会经常使用结构体指针,因此在此做说明

三、指针概念复习

  1. 一级指针 : 指针所存储的是一个地址,一级指针通常用来储存一个变量的地址
#define _CRT_SECURE_NO_WARNINGS  1
#include <stdio.h>

int main() {
	int a = 10;
	int* p = &a;

	printf("%p\n", &a);
	printf("%p\n", p);

	return 0;
}
  1. 二级指针 : 二级指针就是指向指针的指针,也就是说,二级指针所存储的是一个一级指针的地址
#define _CRT_SECURE_NO_WARNINGS  1
#include <stdio.h>

int main() {
	int a = 10;       // 普通变量
	int* p = &a;      // 一级指针,指向 a
	int** pp = &p;    // 二级指针,指向一级指针 p 的地址

	printf("a 的值: %d\n", a);
	printf("a 的地址: %p\n", &a);
	printf("p 的值(a 的地址): %p\n", p);
	printf("pp 的值(p 的地址): %p\n", pp);
	printf("*pp 的值(即 p 的值,即 a 的地址): %p\n", *pp);
	printf("**pp 的值(即 *p 的值,即 a 的值): %d\n", **pp);

	return 0;
}

输出结果
在这里插入图片描述

四、链表

4.1 链表的结构与概念

4.1.1 链表的概念

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

我们之前提到,链表属于线性表的一种。在线性表的逻辑结构中,数据元素是按顺序排列的,因此它在逻辑上是连续的。但在线性表的物理存储方式上,它不一定是连续的。链表就是一种典型的线性表,它采用链式结构,通过指针将各个节点连接在一起,因此在物理存储上不一定需要连续

4.1.2 链表的结构

在这里插入图片描述

4.1.3 链表的分类

链表可以根据单向双向、带头或不带头、循环或非循环组合成8种不同的链表结构

虽然有这么多种链表结构,但实际上最常用的两种结构为

  1. 单向不带头非循环链表

    这种链表的结构较为简单。一般来说不会用来存储数据,通常是用来作为其他数据的子结构,如哈希桶、图的邻接表等。

    不带头意味着我们的头不是一个结构体,而是一个结构体指针。

  2. 双向带头循环链表

    这种链表的结构较为复杂,一般用双向带头循环链表来单独存储数据,实际上使用的链表数据结构都是双向带头循环链表。

    此外,虽然双向带头循环链表的结构较为复杂,但等我们真正实现这种链表后会发现它有很多优势。这里可以先保留一个惊喜,等到我们真正实现时就会知道了。

对于不带头与带头的更多细节,我们会在介绍双向带头循环链表时在进一步比较两者的差异

五、 无头单向非循环链表的基本功能接口

在这边我们仅先对无头单向非循环链表的实现进行演示

5.1 事前准备

在这里插入图片描述

SList.h : 用以定义结构体、声明实现接口的函数,将使用到的库函数集合起来

SList.c : 用以定义实现接口的函数

Test.c : 用以测试接口

5.2 链表结构体定义

typedef int SLTDatatype;

// 链表结构体中主要存储两个东西。1. 当前节点的数据 2. 下一个节点的地址
typedef struct SListNode {
	SLTDatatype data; // 存数据
	struct SListNode* next; // 存下一个节点的地址
    // 这里 struct SListNode 一定完整写出来,因为在C语言中,结构体名称不能当做类型
}SLTNode;
// 这里使用typedef 来为数据类型和结构体重命名,方便后面使用

5.3 链表初始化

SLTNode** plist = NULL; // 初始化

5.4 打印链表

void SLTPrint(SLTNode* phead) {
	SLTNode* cur = phead; // 将cur 指向 phead
	while (cur != NULL) { // 判断当前节点是否为空,不为空就打印
		printf("%d->", cur->data);
		cur = cur->next; // cur指向下一个节点 
        /* 
        为什么是cur = cur->next呢
        我们在定义结构体的时后说过,结构体中储存的是一个数据和下一个节点的地址
        cur = cur->next 相当于让cur的指向变成下一个节点
        */
	}
	printf("NULL\n"); 
}

5.5 创建节点

什么时候会需要用到创建节点?

  1. 尾插
  2. 头插
  3. 在指定位置 (pos) 前/后 插入
SLTNode* Create_node(SLTDatatype x) {
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

六、无头单向非循环链表节点增加功能接口

6.1 尾插

void SLTPushback(SLTNode** pphead, SLTDatatype x) {
	assert(pphead);
	SLTNode* newnode = Create_node(x);

	// 假设只有一个节点
	if (*pphead == NULL) {
		*pphead = newnode;
	}
    // 找尾
	else {
		SLTNode* tail = *pphead;
		while (tail->next != NULL) {
			tail = tail->next;
		}
		tail->next = newnode; // 不是 tail = newnode。tail是局部变量
	}

}

为什么要使用二级指针传递参数?

假设我们有一个空链表,这意味着链表的头指针 plist 指向空地址 0x00000000(即 NULL)。
当我们尝试向链表尾部插入一个新节点时,能直接把它挂在这个 NULL 后面吗?答案是否定的,因为 0x00000000 这个地址是操作系统受保护的区域,我们无法随意访问或修改它。因此,我们不能在 NULL 后插入新节点,而是需要让头指针 plist 直接指向新创建的节点,使其成为链表的第一个节点。

为什么需要二级指针?

要让 plist 指向新节点,我们实际上要修改 plist 本身的值,让它存储新节点的地址。然而,C 语言中的函数参数是值传递,如果我们只使用一级指针(即 STLNode* pphead),那么 pphead 只是原始头指针的一份拷贝。在函数内部修改这个拷贝的值,并不会影响原来的 plist,因此链表的头指针仍然指向 NULL,新节点就无法被正确添加。

因此我们需要传递 plist 的地址,即使用二级指针 STLNode** pphead。这样,函数内部对 *pphead 的修改就会直接影响到plist ( 当然前提是要传递 plist 的地址。

还是不明白的话可以往上看一下指针概念的复习

6.2 头插

void SLTPushfront(SLTNode** pphead, SLTDatatype x) {
	assert(pphead); // 这里可以断言一下,避免有人传递错误的内容 ( pphead一定不为空 )
	SLTNode* newnode = Create_node(x); // 创建节点
	newnode->next = *pphead; //将新创节点指向原头节点
	*pphead = newnode; // 头节点变成新创的节点
}

6.3 在pos位置前 / 后插入

6.3.1 pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x) {
    assert(pos);
	assert(pphead); // 一样断言ppheaad -- pphead 一定不为空
	
    // 这个语句意味着pos的位置是头节点,在头节点前面插入新节点相当于头插
	if (pos == *pphead) { 
		SLTPushfront(pphead, x); // 调用头插的函数
	}
	else {
		// 找到pos的前一个位置
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		SLTNode* newnode = Create_node(x);
		prev->next = newnode;
		newnode->next = pos;
        /*
        上面这段代码的解释是:
        因为我们是要在pos前面插入新的节点,所以势必得要知道原本哪个节点所指向的下一个节点是pos
        这样才能去进行指向的修改
        */
	}
}
6.3.2 pos位置后插入
// 在pos位置后插入就相对简单很多了。
void SLTInsert_after(SLTNode* pos, SLTDatatype x) {
	assert(pos);
	SLTNode* newnode = Create_node(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

七、无头单向非循环链表节点删除功能接口

7.1 尾删

void SLTPopback(SLTNode** pphead) {
	assert(pphead);
	assert(*pphead);
	SLTNode* tail = *pphead;

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

7.2 头删

void SLTPopfront(SLTNode** pphead) {
	assert(pphead);
	assert(*pphead);
	SLTNode* cur = *pphead;
	*pphead = cur->next;
	free(cur);
	cur = NULL;
}

7.3 在pos位置删除 / 在pos位置后删除

7.3.1 在pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos) {
	assert(pphead);
	assert(pos);
	if (*pphead == pos) {
		SLTPopfront(pphead);
	}
	SLTNode* prev = *pphead;
	while (prev->next != pos) {
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
}
7.3.2 在pos位置后删除
void SLTErase_after(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

八、无头单向非循环链表节点查找功能接口

SLTNode* SLTFind(SLTNode* phead, SLTDatatype x) {
	SLTNode* cur = phead;
	while (cur) {
		if (cur->data == x) {
			return cur;
		}
		else {
			cur = cur->next;
		}
	}
	return NULL;
}

九、完整代码与执行结果

9.1 完整代码

9.1.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;
// 打印
void SLTPrint(SLTNode* phead);
//创建节点
SLTNode* Create_node(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 SLTErase(SLTNode** pphead, SLTNode* pos);
// 在pos位置之后插入
void SLTInsert_after(SLTNode* pos, SLTDatatype x);
// 在pos位置之前删除
void SLTErase_after(SLTNode* pos);
9.2.2 SList.c
#define _CRT_SECURE_NO_WARNINGS  1

#include "SList.h"

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

SLTNode* Create_node(SLTDatatype x) {
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {
		perror("malloc fail"); // 只有系统API可以使用perror
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}
void SLTPushback(SLTNode** pphead, SLTDatatype x) {
	assert(pphead);
	SLTNode* newnode = Create_node(x);

	// 找尾
	if (*pphead == NULL) {
		*pphead = newnode;
	}
	else {
		SLTNode* tail = *pphead;
		while (tail->next != NULL) {
			tail = tail->next;
		}
		tail->next = newnode; // 不是 tail = newnode。tail是局部变量
	}

}
void SLTPushfront(SLTNode** pphead, SLTDatatype x) {
	assert(pphead);
	SLTNode* newnode = Create_node(x);
	newnode->next = *pphead;
	*pphead = newnode;
}



void SLTPopback(SLTNode** pphead) {
	assert(pphead);
	assert(*pphead);
	SLTNode* tail = *pphead;

	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL; // free完记得要设成空 不然可能会有野指针问题
	}
	else {
		SLTNode* tail = *pphead;
		SLTNode* prev = *pphead;
		while (tail->next != NULL) {
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		tail = NULL;
		prev->next = NULL;
	}
}
void SLTPopfront(SLTNode** pphead) {
	assert(pphead);
	assert(*pphead);
	SLTNode* cur = *pphead;
	*pphead = cur->next;
	free(cur);
	cur = NULL;

}

SLTNode* SLTFind(SLTNode* phead, SLTDatatype x) {
	SLTNode* cur = phead;
	while (cur) {
		if (cur->data == x) {
			return cur;
		}
		else {
			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 = Create_node(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}
// pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos) {
	assert(pphead);
	assert(pos);
	if (*pphead == pos) {
		SLTPopfront(pphead);
	}
	SLTNode* prev = *pphead;
	while (prev->next != pos) {
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
}

// pos后插入
void SLTInsert_after(SLTNode* pos, SLTDatatype x) {
	assert(pos);
	SLTNode* newnode = Create_node(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
// pos后删除
void SLTErase_after(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

9.2 测试结果

9.2.1 尾插测试
// 尾插测试
void SList_Test1() {
	SLTNode** plist = NULL; // 初始化
	SLTPushback(&plist, 1);
	SLTPushback(&plist, 2);
	SLTPushback(&plist, 3);
	SLTPushback(&plist, 4);

	SLTPrint(plist);
}

输出结果

img

9.2.2 尾删测试
// 尾删测试
void SList_Test2() {
	SLTNode** plist = NULL; // 初始化
	SLTPushback(&plist, 1);
	SLTPushback(&plist, 2);
	SLTPushback(&plist, 3);
	SLTPushback(&plist, 4);

	SLTPrint(plist);

	SLTPopback(&plist);
	SLTPrint(plist);
	SLTPopback(&plist);
	SLTPrint(plist);
	SLTPopback(&plist);
	SLTPrint(plist);
	SLTPopback(&plist);
	SLTPrint(plist);
}

输出结果

img

9.2.3 头插测试
void SList_Test3() {
	SLTNode** plist = NULL;

	SLTPushfront(&plist, 4);
	SLTPushfront(&plist, 3);
	SLTPushfront(&plist, 2);
	SLTPushfront(&plist, 1);

	SLTPrint(plist);
}

输出结果

img

9.2.4 头删测试
void SList_Test4() {
	SLTNode** plist = NULL;

	SLTPushfront(&plist, 4);
	SLTPushfront(&plist, 3);
	SLTPushfront(&plist, 2);
	SLTPushfront(&plist, 1);

	SLTPrint(plist);

	SLTPopfront(&plist);
	SLTPrint(plist);

	SLTPopfront(&plist);
	SLTPrint(plist);

	SLTPopfront(&plist);
	SLTPrint(plist);

	SLTPopfront(&plist);
	SLTPrint(plist);
}

输出结果

img

9.2.5 在pos前 / 后插入及查找测试
// 1.
void SList_Test5() {
	SLTNode** plist = NULL;

	SLTPushfront(&plist, 4);
	SLTPushfront(&plist, 3);
	SLTPushfront(&plist, 2);
	SLTPushfront(&plist, 1);
	// 假设我们在2前插入 5
	SLTNode* ret = SLTFind(plist, 2);
	SLTInsert(&plist, ret, 5);
	SLTPrint(plist);
}
// 2.
void SList_Test5() {
	SLTNode** plist = NULL;

	SLTPushfront(&plist, 4);
	SLTPushfront(&plist, 3);
	SLTPushfront(&plist, 2);
	SLTPushfront(&plist, 1);
	// 假设我们在3后插入 5
	SLTNode* ret = SLTFind(plist, 3);
	SLTInsert_after(ret, 5);
	SLTPrint(plist);
}

输出结果

  1. 在pos前插入

    img

  2. 在pos后插入

    img

9.2.6 在pos位置删除 / 在pos位置后删除测试
// 1. 在pos位置删除
void SList_Test6() {
	SLTNode** plist = NULL;

	SLTPushfront(&plist, 4);
	SLTPushfront(&plist, 3);
	SLTPushfront(&plist, 2);
	SLTPushfront(&plist, 1);
	// 假设我们要删除2这个节点
	SLTNode* ret = SLTFind(plist, 2);
	SLTErase(&plist, ret);
	SLTPrint(plist);
}
// 2. 在pos位置后删除
void SList_Test6() {
	SLTNode** plist = NULL;

	SLTPushfront(&plist, 4);
	SLTPushfront(&plist, 3);
	SLTPushfront(&plist, 2);
	SLTPushfront(&plist, 1);
	// 假设我们要删除2后面的节点
	SLTNode* ret = SLTFind(plist, 2);
	SLTErase_after(ret);
	SLTPrint(plist);
}

输出结果

  1. 在pos位置删除

    img

  2. 在pos位置后删除

    img

以上为这次的内容。如果内容有错还请不吝啬的提出,看到会尽快改正!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值