系列文章目录
数据结构之链表
一、链表的介绍
1.1 概念
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
链表由一个个结点构成,每个结点包括两个区域:
- 数据域
- 指针域
1.2 类别
不同于顺序表,链表有多个形式,区别如下:
- 头指针/头节点
- 单向/双向
- 循环/不循环
尽管链表有多种形式,但最常用的是以下两种
二、链表的实现
以无头单向链表为例
2.1 总体设计
SLTList.c
问题
- 为什么参数为二级指针?
答:对于无头链表,它使用的是头指针(phead),链表为空时,phead指向NULL,插入元素后,phead指向一个结点,因此对于可能要改变phead的值的函数(如插入,删除),要进行传址调用,也就是需要传二级指针,对于不需要改变phead的值的函数(如查找,打印),传一级指针就可以了。- 什么函数需要进行断言检测?(断言即assert()库函数)?
答:传入的参数可能是野指针的函数。对于插入函数,本身就要考虑链表为空的情况,无需断言。删除/更改函数要判断链表是否为空,需要断言。
注:二级指针是不可能为NULL。
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
//新增结点 - 这个函数只是为了方便,以免后面每个插入函数都要写这几行代码
static SLT* AddNode(SLTDateType x) {
SLT* newnode = (SLT*)malloc(sizeof(SLT));
assert(newnode != NULL);
newnode->date = x;
newnode->next = NULL;
return newnode;
}
//打印
void SLTPrint(SLT* phead)
{
SLT* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->date);
cur = cur->next;
}
printf("NULL");
}
//插入
//1.尾插
void SLTPushBack(SLT** pphead, SLTDateType x)
{
//情况:链表为空或不为空
SLT* newnode = AddNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLT* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
//2.头插
void SLTPushFront(SLT** pphead, SLTDateType x)
{
//情况:链表为空或不为空
SLT* newnode = AddNode(x);
if (*pphead == NULL) {
*pphead = newnode;
}
else {
newnode->next = *pphead;
*pphead = newnode;
}
}
//3.指定插 - 在pos前插入
void SLTInsert(SLT** pphead, SLT* pos, SLTDateType x)
{
assert(pos);//pos可能为NULL,这里为了以防万一
//这里 pos 传的类型是 SLT*,当然pos也可以是int类型, 这里是为了对应C++
SLT* newnode = AddNode(x);
if (pos == *pphead)
{
newnode->next = pos;
*pphead = newnode;
}
else
{
//找posprev 意思是(position+previous) - 简写为 posprev
SLT* posprev = *pphead;
while (posprev->next != pos)
{
posprev = posprev->next;
}
posprev->next = newnode;
newnode->next = pos;
}
}
//删除
//1.头删
void SLTPopFront(SLT **pphead)
{
//链表为空时
if (*pphead == NULL)
{
printf("链表为空,无需删除\n");
return;
}
//链表不为空
SLT* tmp = *pphead;
*pphead = (*pphead)->next;
free(tmp);
tmp = NULL;
}
//2.尾删
void SLTPopBack(SLT** pphead)
{
if (*pphead == NULL)
{
printf("链表为空,无需删除\n");
return;
}
else if ((* pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLT* prev = NULL;
SLT* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
//单链表的缺陷:不能指向前一个
free(tail);
tail = NULL;
prev->next = NULL;
}
}
//3.指定删
void SLTErase(SLT** pphead, SLT* pos)
{
assert(*pphead);
assert(pos);
if (*pphead == pos)
{
SLTPopFront(pphead);
}
else
{
SLT* prev = *pphead;
while (prev->next == pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
//查找
SLT* SLTFind(SLT* phead, SLTDateType x)
{
//查找不用担心phead为NULL,且不用改变phead,因此不用断言
while (phead)
{
if (phead->date == x)
{
return phead;
}
phead = phead->next;
}
return NULL;
}
//更改
void SLTChange(SLT* pos, SLTDateType x)
{
assert(pos);
pos->date = x;
}
//销毁
void SLTDestroy(SLT** pphead)
{
assert(pphead);
while (*pphead != NULL)
{
SLT* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
SLTList.h
#pragma once
//必要头文件
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//单链表结点
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType date;//数据域
struct SListNode* next;//指针域
}SLT;
//基本操作函数
void SLTPrint(SLT* phead);
//插入:尾插,头插,指定位置插
void SLTPushBack(SLT **phead, SLTDateType x);
void SLTPushFront(SLT** phead, SLTDateType x);
void SLTInsert(SLT** phead, SLT* pos, SLTDateType x);//在pos前插入
//删除:头删, 尾删, 指定删
void SLTPopFront(SLT** pphead);
void SLTPopBack(SLT** pphead);
void SLTErase(SLT** pphead, SLT* pos);
//查找
SLT* SLTFind(SLT* pphead, SLTDateType x);
//更改
void SLTChange(SLT* pos, SLTDateType x);
//销毁
void SLTDestroy(SLT** pphead);
Test.c
测试功能,代码随意
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void Test2() {
SLT* plist = NULL;
SLTPushFront(&plist, 5);
SLTPushFront(&plist, 3);
SLTPushBack(&plist, 4);
SLT* pos = SLTFind(plist, 4);
SLTInsert(&plist, pos, 9);
SLTPrint(plist);
}
int main(void)
{
Test2();
return 0;
}
三、链表的优缺点
- 链表的优点:
动态数据结构
链表是一种动态数据结构,因此它可以在运行时通过分配和取消分配内存来增长和缩小。所以没有必要给出链表的初始大小。
易于插入和删除
在链表中进行插入和删除节点真的很容易。与数组不同,我们不必在插入或删除元素后移位元素。在链表中,我们只需要更新节点下一个指针中的地址。
内存利用率高
由于链表的大小可以在运行时增加或减少,因此没有内存浪费。在数组的情况下,存在大量的内存浪费,就像我们声明一个大小为10的数组并且只存储6个元素,那么浪费了4个元素的空间。链表中没有这样的问题,因为只在需要时才分配内存。- 链表结构的缺点
内存的使用
与数组相比,在链表中存储元素需要更多内存。因为在链表中每个节点都包含一个指针,它需要额外的内存。
遍历困难
链表中的元素或节点遍历很困难,访问元素的效率低。我们不能像索引一样随机访问任何元素。例如,如果我们想要访问位置n的节点,那么我们必须遍历它之前的所有节点。因此,访问节点所需的时间很长。
反向遍历困难
在链表中反向遍历非常困难。在双链表的情况下,后指针可以更方便,但需要更多的内存。