前言:
上篇文章介绍了顺序表,这篇文章开始着重讲解链表了。
链表有很多种:单、双链表,循环、非循环链表还有带头、不带头的链表。本篇的主要内容是单链表(无头,单向,非循环)。
链表对比顺序表有哪些不同之处,接下来会带大家一起了解~
一.顺序表的缺陷 && 介绍链表
1.顺序表的缺陷
1.头部和中间的插入删除效率都较低,时间复杂度为O(N)。需要挪动数据。
2.空间不够用了,增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。(尤其是异地扩容)
3.扩容会有一定的空间浪费。(例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间)
2.介绍链表
针对顺序表的缺陷,就有了链表这个数据结构
(1)链表的概念
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
特点:按需申请释放
顺序表是数组存储数据的,空间是连续的(可通过一个指针找到所有的值),通过size标记直到没有数据(前面的为size的个数即有效数据)。
链表的每个节点的大小没有关系,也不连续(多次malloc开辟出来的空间是随机的)。它是通过一个头指针(phead)先找到第一个节点,然后通过第一个节点的指针找到第二个节点,第二个节点的指针找到第三个节点,以此类推(通过指针链接)。每个位置的节点都有指针指向下一个,当下一个为空指针的时候,就结束。
(2)链表的结构
物理图:
逻辑图:
链表的节点组成(单链表):
注意:链表的最后一个节点的next指向空
看到这有些小伙伴可能有些疑惑,链表的每个节点是不连续的,为什么上面的两个图中每个节点都有线连接起来变成看似连续的呢?其实不是这样的,以上的两张图是为了方便理解。实际在内存中每个节点的地址是随机的,只不过用这个节点的指针(next)找到了下一个节点的地址,所以才能实现链接。
(3)链表的功能
链表的功能与顺序表类似,无非是增删查改,在某位置的插入与删除,对数据内容进行管理和操作。
二.单链表的实现
还是以多文件的形式分模块写
SList.h——函数和类型的声明
SList.c——函数的实现
Test.c——进行测试
1.创建节点的结构
单链表一个节点的结构:
存放数据:data
结构体指针:next
注意:不能这样写:
typedef int SListDataType;//方便更改存储的数据类型
typedef struct SListNode
{
SListDataType data;
SLTNode* next;
}SLTNode;// <-重定义开始生效的位置
因为typedef重定义结构体类型的名称是在上面有箭头的一行开始生效,生效了才能使用,在前面就提前使用就会出现错误。
正确写法:
typedef int SListDataType;//方便更改存储的数据类型
typedef struct SListNode
{
SListDataType data;
struct SListNode* next;
}SLTNode;
2.头文件函数的声明
1.打印单链表
2.创建一个节点
3.尾插
4.头插
5.尾删
6.头删
7.查找(包含修改)
8.在pos位置前插入
9.在pos位置后插入
10.删除pos位置的节点
11.删除pos位置后一个的节点
12.清理单链表
//打印单链表
void SLTPrint(SLTNode* phead);
//创建一个节点
SLTNode* BuySLTNode(SListDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SListDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SListDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SListDataType x);
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x);
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x);
//删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置后一个的节点
void SLTEraseAfter(SLTNode* pos);
//清理单链表
void SLTDestroy(SLTNode** pphead);
3.函数的实现
(1)打印单链表
创建一个结构体指针变量(cur),使它指向第一个节点(把头指针覆给cur)。利用循环如果cur不是空指针,就打印cur所指向的数据,然后cur往后走(到下一个节点)。直到cur为空跳出,最后打印NULL(最后一个节点为空指针)。
逻辑图:
物理图:
注意:与顺序表不同,顺序表传过来的指针一定不为空;链表传过来的指针可能为空,比如链表没有节点,头指针指向的就是NULL,所以不需要断言头指针。
所以在测试的文件里(Test.c)刚开始要让头指针指向NULL
SLTNode * plist = NULL;
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
(2)创建一个节点
为了方便后面的尾插、头插等操作,所以写个函数来创建一个新节点。
新节点的类型也是结构体指针,用malloc函数开辟一个新节点。如果新节点为空就报错。然后给新节点的data赋值,next为空,返回这个节点(方便其他的函数使用)
SLTNode* BuySLTNode(SListDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
(3)尾插
尾插一个新节点,假设有多个节点,首先要找到尾,定义一个变量tail去遍历链表找到尾
注意:用tail遍历找尾再尾插时不能写成:
SLTNode* tail = *pphead;
while (tail)
{
tail = tail->next;
}
tail = newnode;
这段代码看似没有什么问题,其实是与正确的代码差别很大。
tail刚开始指向第一个节点,如果不为空,到下一个节点;当tail为空时跳出循环,把newnode的值(新节点的地址)赋给tail。
如图:
这里有一个问题,tail里面存放的是新节点的地址,但是原来链表的最后一个节点的next指针并没有存放新节点的地址,也就是说最后一个节点没有与新节点连接起来,就没有尾插了。其次还有可能存在内存泄漏,新创建的节点丢了。
因为tail是局部变量,newnode和phead也是,它们出这个函数就销毁了,所以给tail这个变量赋新节点的地址没有用。
要成功完成尾插,就必须改变结构体的内容,让最后一个节点的next指针指向新节点的地址。
这里大家可能有些疑惑,既然tail销毁了,那么链表的这些节点会不会销毁呢?
答案是不会,因为这些节点是malloc出来的,malloc在堆上开辟的空间,只有自己主动free释放掉才能销毁。
正确的思路:
首先想到的是要改变结构体(节点)的内容,那么tail这个指针变量就不能到空结束,而是到最后一个节点结束(tail的next为空就结束,tail的位置指向最后一个节点)。
此时尾节点的next为空,我们要做的是让尾节点的next存放新节点的地址。让tail的next存放newnode的值(新节点的地址),就可以改变结构体的内容。
找尾尾插正确的一小段代码:
SLTNode* tail = *pphead;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
还有一种情况,如果刚开始链表没有节点,就不需要找尾了。直接将新节点的地址给头指针(plist)就行
但是这种情况要注意什么呢?
以下是错误示范:
if (phead == NULL)
{
phead = newnode;
}
这个代码的意思如图所示:
有两个问题:
一:plist没有改变,还是指向空指针;新节点丢了,可能造成内存泄漏。
二:newnode和phead是形参,形参是实参的拷贝,出这个函数就销毁了,改变phead并没有改变plist。
注意!!!:plist是一级指针,改变一级指针需要用到二级指针,并且有解引用操作。所以在函数的参数应该用二级指针来接收(传参时plist要有取地址符才能与二级指针类型对应)
正确的一小段代码:
if (*pphead == NULL)
{
*pphead = newnode;
}
总结:
1.改变结构体,要用结构体指针
2.改变结构体指针,要有结构体指针的指针(即二级指针)
最后一点:什么时候要断言指针
当一级指针(* pphead)为空时不需要断言,因为如果刚开始链表没有节点,* pphead所指向的就是空指针。二级指针pphead存放的是一级指针的地址,一级指针的地址不可能为空,所以二级指针需要断言。
void SLTPushBack(SLTNode** pphead, SListDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
//原来没有节点,改变结构体指针,用二级指针
if (*pphead == NULL)
{
*pphead = newnode;
}
//原来有节点,改变结构体,用结构体指针
else
{
SLTNode* tail = *pphead;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
(4)头插
头插也需要用到二级指针,因为每次头插头指针(plist)都要连接新的节点。(改变了头指针)
头插时原来链表没有节点与原来链表有节点的思路是一样的
新节点先连接第一个节点或者空指针,然后plist再连接新节点
注意:两者的顺序不能换,因为如果先让plist连接newnode,那么原来链表plist头指针后面的节点就找不到了。newnode再连接plist所指向的下一个节点就是自己,导致死循环。
void SLTPushFront(SLTNode** pphead, SListDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
(5)尾删
前面的尾插、头插都有用到二级指针,那么尾删需不需要二级指针呢?接下来我们一点一点的分析:
尾删的大体思路是:找到尾,然后free释放掉尾节点就行。
但是链表有一个很重要的点:前后关联
这里我们定义一个指针变量tail去找尾,把尾节点删掉了,那么原来前一个节点变成新的尾节点,还需要用另一个变量当作原来尾节点的前一个节点,新的尾节点next指针就必须指向NULL(只需要改变结构体),否则就访问野指针了。
有两种写法,这里只展现一种,就用tail一个指针变量,让它的下一个的下一个指针为空时停下(tail->next->next==NULL),此时tail->next就是最后一个节点,tail是前一个节点,修改新的尾节点的next,让tail->next为NULL(改变结构体)就行了。
以上只是包括一类情况:一个以上节点的时候是这样的
如果尾删把节点只删到剩下一个节点时,还是如此吗?
按前面的思路来走,遇到尾节点就把它的前一个节点的next置空。
依图分析,只有一个节点时,前一个节点就不是节点了,是头指针。要让头指针指向NULL,即改变头指针,就要用到二级指针了。
让 * pphead置空,就可以改变头指针
plist 等价于 * pphead
没有节点的情况:
断言 * pphead,为空就不能再删了
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
//空
assert(*pphead);
//一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//一个以上的节点
else
{
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
(6)头删
通过前面的分析发现,有改变头指针所指向的内容就要用到二级指针,头删是把第一个节点除去,让头指针指向新的头节点。
画图分析:
当链表没有节点时不能再删了,所以要对 * pphead断言( * pphead等价于plist即第一个节点)
只有一个节点和有多个节点不需要分开处理,定义一个变量记录原来链表的第二个节点(新的头节点),free释放掉第一个节点,让头指针连接新的头节点
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
//空
assert(*pphead);
//非空
SLTNode* newhead = (*pphead)->next;//注意优先级
free(*pphead);//不需要置空,因为头指针直接连接新的头
*pphead = newhead;
}
(7)查找
定义一个变量cur遍历链表,先判断cur所指向的数据是否等于x,如果相等,返回cur,否则往后走;找不到返回空指针。
SLTNode* SLTFind(SLTNode* phead, SListDataType x)
{
assert(phead);
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
查找可以包含修改这个节点的数据
SLTNode* pos1 = SLTFind(plist, 2);//测试查找+修改
if (pos1 != NULL)
{
printf("找到了\n");
pos1->data *= 100;
SLTPrint(plist);
}
else
{
printf("找不到\n");
}
(8)在pos位置前插入
要在pos位置前插入一个新节点,首先pos这个位置的节点必须存在,所以要断言pos(后面有pos位置插入删除的函数也要用到)
pos可能在任意一个位置,如果pos在第一个节点,就相当于头插了。头插要改变头指针所指向的内容,所以要用二级指针。直接调用头插的函数即可。
pos不在第一个节点的情况:
首先要定义一个变量prev,遍历链表找到并指向pos的前一个节点,因为插入新的节点必须前后连接起来(单链表的不足之处,后期文章用双向循环带头链表就非常简单)。
当prev->next != pos,往后走;==pos时跳出循环,让prev->next连接新节点,新节点的next连接pos,完成插入。
void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x)
{
assert(pphead);
assert(pos);
//pos在第一个节点就是头插
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
(9)在pos位置后插入
因为在pos位置后插入新的节点,所以可以不用头指针了,找到pos位置的下一个节点即可。可以定义一个变量posNext为pos位置的下一个节点,让新节点的next连接posNext,pos->next连接新节点
不需要考虑是不是尾插,因为在哪插入都是一样的
void SLTInsertAfter(SLTNode* pos, SListDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
SLTNode* posNext = pos->next;
newnode->next = posNext;
pos->next = newnode;
}
(10)删除pos位置
删除pos位置的节点,必须把它的前一个节点与后一个节点连接起来,这里就要有头指针,去找pos位置的前一个节点。
我们要考虑一些情况,pos在第一个节点、中间某个节点和尾节点
当pos在第一个节点时,就是头删,要改变头指针指向的内容,所以要用二级指针,然后调用头删的函数即可
如果pos是在中间的某个节点或者尾节点呢?
其实两者的思路是一致的,把pos位置的节点删除,让前一个节点连接后一个节点就行(是尾节点的话,让前一个节点连接空指针)
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(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;
}
}
(11)删除pos位置后的节点
要删除pos位置的后一个节点,除了pos这个位置要存在之外,pos位置的后一个节点也必须存在,所以pos->next要断言。假如pos是在尾节点,就没有意义了。
定义一个变量posNext为pos的下一个节点,然后使pos->next指向posNext->next,即把pos位置的节点与posNext的下一个节点连接起来,最后释放掉posNext
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);//检查是否为尾节点
SLTNode* posNext = pos->next;
pos->next = posNext->next;
free(posNext);
posNext = NULL;
}
(12)清理单链表
清理(销毁)链表,必须要一个一个节点清理,因为链表在物理结构上是不连续的。
定义一个变量cur遍历链表,每到一个节点把它释放掉。但是这里又有一个问题,当前节点被释放了,怎么到下一个节点呢?所以我们循环里再定义一个变量next为cur的下一个节点,释放完当前的cur,然后把next赋给cur,这样cur就能到下一个节点了。
最后全部节点释放完,头指针要指向空,这里又有改变头指针了,所以有二级指针。
文章来源:https://www.toymoban.com/news/detail-659567.html
void SLTDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
三.全部代码
1.SList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SListDataType;//方便更改存储的数据类型
typedef struct SListNode
{
SListDataType data;
struct SListNode* next;
}SLTNode;
//打印单链表
void SLTPrint(SLTNode* phead);
//创建一个节点
SLTNode* BuySLTNode(SListDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SListDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SListDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SListDataType x);
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x);
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x);
//删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置后一个的节点
void SLTEraseAfter(SLTNode* pos);
//清理单链表
void SLTDestroy(SLTNode** pphead);
2.SList.c
#include "SList.h"
//打印
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
//创建一个节点
SLTNode* BuySLTNode(SListDataType 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, SListDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
//原来没有节点,改变结构体指针,用二级指针
if (*pphead == NULL)
{
*pphead = newnode;
}
//原来有节点,改变结构体,用结构体指针
else
{
SLTNode* tail = *pphead;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
//头插
void SLTPushFront(SLTNode** pphead, SListDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
//空
assert(*pphead);
//一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//一个以上的节点
else
{
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
//空
assert(*pphead);
//非空
SLTNode* newhead = (*pphead)->next;//注意优先级
free(*pphead);//不需要置空,因为头指针直接连接新的头
*pphead = newhead;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SListDataType x)
{
assert(phead);
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x)
{
assert(pphead);
assert(pos);
//pos在第一个节点就是头插
if (pos == *pphead)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
SLTNode* posNext = pos->next;
newnode->next = posNext;
pos->next = newnode;
}
//删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(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)
{
assert(pos);
assert(pos->next);//检查是否为尾节点
SLTNode* posNext = pos->next;
pos->next = posNext->next;
free(posNext);
posNext = NULL;
}
//清理
void SLTDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
3.Test.c
#include "SList.h"
test()
{
SLTNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);//测试尾插
SLTPrint(plist);
SLTPushFront(&plist, 10);
SLTPushFront(&plist, 20);
SLTPushFront(&plist, 30);
SLTPushFront(&plist, 40);//测试头插
SLTPrint(plist);
SLTPopBack(&plist);
SLTPopBack(&plist);
SLTPopBack(&plist);//测试尾删
SLTPrint(plist);
SLTPopFront(&plist);
SLTPopFront(&plist);//测试头删
SLTPrint(plist);
SLTNode* pos1 = SLTFind(plist, 2);//测试查找+修改
if (pos1 != NULL)
{
printf("找到了\n");
pos1->data *= 100;
SLTPrint(plist);
}
else
{
printf("找不到\n");
}
SLTNode* pos2 = SLTFind(plist, 10);//测试pos位置前插入
if (pos2)
{
SLTInsert(&plist, pos2, 66);
SLTPrint(plist);
}
SLTNode* pos3 = SLTFind(plist, 20);//测试pos位置后插入
if (pos3)
{
SLTInsertAfter(pos3, 77);
SLTPrint(plist);
}
SLTNode* pos4 = SLTFind(plist, 1);//测试删除pos位置
if (pos4)
{
SLTErase(&plist, pos4);
SLTPrint(plist);
}
SLTNode* pos5 = SLTFind(plist, 66);//测试删除pos位置的后一个节点
if (pos5)
{
SLTEraseAfter(pos5);
SLTPrint(plist);
}
SLTDestroy(&plist);
}
int main()
{
test();
return 0;
}
总算把最费劲的写完了,感谢铁子们的观看,期待大家的支持~文章来源地址https://www.toymoban.com/news/detail-659567.html
到了这里,关于【数据结构】吃透单链表!!!(详细解析~)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!