数据结构——单链表
🏖️专题:数据结构
🙈作者:暴躁小程序猿
⛺简介:双非本科大二小菜鸟一枚,希望大佬们指点~
前言
上一篇博客讲了什么是顺序表,顺序表的增删改查等基本操作功能,但是顺序表有以下缺点:
1.中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间
如何解决这些缺点呢?我们就要讲到链表了。
一、链表是什么?
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
逻辑结构如下图:
物理结构:
二、单链表的功能实现
1.头文件
代码如下(示例):
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLNodeType;
struct SList
{
SLNodeType data;
struct SList* next;
};
typedef struct SList SLTNode;
//申请新的结点
SLTNode* BuySLTNode(SLNodeType x);
//打印
void SLPrint(SLTNode* pphead);
//头插法
void SLPushFront(SLTNode** pphead, SLNodeType x);
//尾插法
void SLPushBack(SLTNode** pphead, SLNodeType x);
//头删法
void SLPopFront(SLTNode** pphead);
//尾删法
void SListPopBack(SLTNode** pphead);
//查找
SLTNode* Find(SLTNode* phead, SLNodeType x);
//删除位置为pos的结点
void SListErase(SLTNode** pphead, SLTNode* pos);
//在pos位置之前插入一个结点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLNodeType x);
头文件里面写的是结构体的定义和函数的声明,里面涉及到增删改查等基本操作,增加涉及头加,尾加,指定位置插入,删除涉及头删,尾删,指定位置删除。
2.函数功能文件
代码如下(示例):
#define _CRT_SECURE_NO_WARNINGS
#include"SList.h"
SLTNode* BuySLTNode(SLNodeType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SLPrint(SLTNode* pphead)
{
SLTNode* cur = pphead;
while (cur!= NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//头插法
void SLPushFront(SLTNode** pphead, SLNodeType x)
{
assert(pphead);
SLTNode* newnode=BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾插法
void SLPushBack(SLTNode** pphead, SLNodeType x)
{
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
//头删法
void SListPopFront(SLTNode** pphead)
{
SLTNode* next = (*pphead)->next;
SLTNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
//尾删法
void SListPopBack(SLTNode** pphead)
{
if (*pphead == NULL)
{
return;
}
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
//查找
SLTNode* Find(SLTNode* phead,SLNodeType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
//在pos位置之前插入一个结点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLNodeType x)
{
SLTNode* newnode = BuySLTNode(x);
if (pos == *pphead)
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
//删除位置为pos的结点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
if (pos == *pphead)
{
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
这个文件里面写的就是具体功能实现的代码,接下来具体分析:
2.1.1创建一个新的结点
SLTNode* BuySLTNode(SLNodeType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
这个函数的功能是创建一个新的结点,因为顺序表的创建会造成一些空间上的浪费,如果一个一个创建每次都要找到合适的连续空间存储,会消耗大量时间,所以我们使用链表的优点就是每增加一个新的结点才申请创建一个结点,不会造成资源的浪费,同时链表的优点就是不需要连续的物理空间,我们申请空间之后先判断malloc函数的返回值是一个正确的地址还是一个空地址,如果是空地址的话我们就使用perror函数将错误打印出来,如果成功申请我们就将数据放入申请的新节点的data数据域,然后将新结点的指针域置为NULL。
2.1.2头插法
//头插法
void SLPushFront(SLTNode** pphead, SLNodeType x)
{
assert(pphead);
SLTNode* newnode=BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
头插法要注意的就是函数的形式参数第一个要用二级指针来接收,如果使用一级指针来接受的话函数内部确实改变了,但是真正链表却不会发生任何改变,因为形式参数如果按值传递只是实参的一份临时拷贝,形参的改变是不会影响到我们的实参的,所以我们要传过去一个地址,我们的链表地址要用二级指针接收。
2.1.3 尾删法
//尾删法
void SListPopBack(SLTNode** pphead)
{
if (*pphead == NULL)
{
return;
}
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
我们使用尾删法要注意一些特殊情况:
1.如果本身就是一个空的链表
if (*pphead == NULL)
{
return;
}
如果是这种情况我们就直接结束就可以。
2.如果链表本身就只有一个结点,首元结点就是尾部的结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
我们就直接释放链表,然后将链表置为NULL
3.其余正常情况下,我们就需要两个指针一个prev指针一个tail指针,我们判断tail->next是否是NULL,如果不是我们就将tail赋给prev,同时tail后移,继续如此循环,如果tail->next==NULL,那么prev就将是新的最后一个尾结点,我们释放tail指针,然后将prev的指针域置为NULL。
//尾删法
void SListPopBack(SLTNode** pphead)
{
if (*pphead == NULL)
{
return;
}
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else {
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
2.1.4在pos位置之前插入一个结点
//在pos位置之前插入一个结点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLNodeType x)
{
SLTNode* newnode = BuySLTNode(x);
if (pos == *pphead)
{
SLPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
我们先判断一下pos位置的结点是不是链表的首元结点,如果是首元结点我们直接头部插入一个新节点就可以,如果正常情况我们还是需要一个prev指针,如果prev->next==pos,我们就将新结点的地址给prev的指针域,然后将pos给新结点的指针域,完成插入。
其余功能很容易实现。
总结
本篇博客讲了单链表的概念,逻辑结构图和物理结构图和具体功能的实现,链表一共可以分为8钟,这只是其中一种,会陆续在博客上分享给大家,欢迎大家私信,我们明天见~