文章目录
数据结构中单链表属于基础知识,所谓大厦建立始于地基,基础更加需要牢固地掌握。
提示:以下是本篇文章正文内容,下面案例可供参考
一、单链表的基本介绍
单链表是一种链式存取的数据结构,单链表中的数据是以结点的形式存在,每一个结点是由数据元素和下一个结点的存储的位置组成。单链表与数组相比的最大差别是:单链表的数据元素存放在内存空间的地址是不连续的,而数组的数据元素存放的地址在内存空间中是连续的,这也是为什么根据索引无法像数组那样直接就能查询到数据元素。
但这种不连续的特点为其数据的插入与删除也提供了便捷
二、单链表的实现(带哨兵位)
1.带表头(哑结点/哨兵位)的好处
在没有表头的情况下,涉及到头结点的删除与插入时,必须考虑到NULL在修改时可能导致的编程错误,但加入表头之后,则可以更加简单地解决这个问题。
当然,表头的使用与否取决于个人,部分人认为,添加假想的单元只是为了避免特殊情况,这样的理由并不充分,他们把表头的使用看成和老式的随意删改没有区别。不过,以上争论并不在本篇讨论范围内。
2.基本结构体的建立
为了方便展示链表,数据的存储此处简单的使用了一个int来代替,在实际的使用中,则往往是利用结构体保存数据。
typedef int SLlistData;//(模拟作为存储数据的结构体)保存数据
typedef struct SLlist {
SLlistData Data;
struct SLlist* next;
}SLlist;
3.表头的建立(初始化)
在建立链表之前,我们往往将函数的声明放在一个单独的头文件中,函数的定义置于另一.c文件(include包含该.h文件),最后建立test.c文件执行
- SLlits.h文件内部
SLlist* InitSLlist(SLlist *node);//初始化头结点,返回值为结构体地址,
- SLlist.c文件内部
#include"Slist.h"
SLlist* InitSLlist(SLlist* node) {
node = (SLlist*)malloc(sizeof(SLlist));
if (node == NULL)
{
perror("malloc error:");
exit(-1);//开辟失败,终止程序
}
node->next = NULL;
return node;
}
值得一提的是,此处不可以利用realloc来代替malloc开辟空间,realloc会直接初始化node的节点地址为0,即NULL,导致开辟失败。
- test.c文件内部
#include"Slist.h"
int main()
{
SLlist* Head=NULL;//建立表头
Head=InitSLlist(Head);
}
4.创建节点元素与查找
- 创建元素节点
SLlist* BuySListnode(SLlistData x) {
SLlist* node = (SLlist*)malloc(sizeof(SLlist));
if (node == NULL)
{
perror("realloc error:");
exit(-1);//开辟失败,终止程序
}
node->Data = x;
node->next = NULL;
return node;
}
- 创建Find函数(Find函数往往与后面的Print(SLlistprint)函数一起使用
SLlist* FindSLlistnode(SLlist* pHead,SLlistData x) {//cur指current
assert(pHead);//因为带表头,而表头不被允许为NULL所以要进行断言
SLlist* cur;
cur = pHead->next;
while (cur)
{
if (cur->Data == x)
return cur;
else
cur = cur->next;
}
return NULL;
}
注意:因为带表头,而表头不被允许为NULL所以要进行断言(或者采取更加温和的方式进行检查)
5.头插头删
表头使用的好处在此处可以很好地体现出来,实现相应功能的代码能清楚的表达基本的指针操作,而不至于因特殊情况而导致含糊不清
- 头插
void PushFront(SLlist* pHead, SLlistData x) {//头插
assert(pHead);
SLlist* node=(SLlist*)malloc(sizeof(SLlist));
if (node == NULL)
{
perror("malloc error:");
exit(-1);//开辟失败,终止程序
}
node = BuySListnode(x);
node->next = pHead->next;
pHead->next = node;
}
- 头删
void PopFront(SLlist* pHead) {
assert(pHead);//注意此处的pHead指向表头,非真的头结点,不可为NULL
if (pHead->next == NULL)
return;
else
pHead->next = pHead->next->next;
}
6.尾插尾删
- 尾插
- BuySLlistnode(x)创建时next就已经置空,因此后面不必再对next进行操作
- 真实的头结点可能为NULL,所以需要考虑到一开始头(head)节点就是尾(tail)节点的情况,此时cur可能已经为NULL,则NULL->next的操作非法,因此while内条件采取合取式
void PushBack(SLlist* pHead, SLlistData x) {
assert(pHead);
SLlist* cur=pHead->next;
SLlist* node = BuySLlistnode(x);//创建时next就已经置空
while (cur&&cur->next)//真实的头结点可能为NULL,所以需要考虑到一开始头(head)节点就是尾(tail)节点的情况
{
cur = cur->next;
}
if (NULL == cur)
pHead->next = node;
else
cur->next = node;
}
- 尾删
真头结点为空时也用不上prev,所以prev直接赋为头节点
void PopBack(SLlist* pHead) {
assert(pHead);
SLlist* cur = pHead->next;
SLlist* prev = pHead->next;//真头结点为空时也用不上prev,所以直接赋为头节点
while (cur && cur->next)
{
prev = cur;//保存上一节点
cur = cur->next;
}
if (cur == NULL)
return;
else
prev->next = NULL;
}
7.删除与插入
- 删除
值得一提的是,此处free(position)与置空在此处并不起作用,究其根本,在于position在此处本质为地址的形参,其修改不会影响实参。所以,该指针的置空应当在调用完函数后再对实参进行执行/在一开始就传入position的地址(二级指针)以完成对一级指针的修改
- 还需要注意的是,free的作用是将该数字(转化成十六进制)表示的地址释放,所以此处position虽是形参,但表示的内容是外部实参的地址,因此free在此处是可以释放实参的
void Erasenode(SLlist* pHead, SLlist* position) {
assert(pHead);
assert(position);
SLlist* cur = pHead->next;
SLlist* prev = pHead->next;
while (cur != position)
{
prev = cur;
cur = cur->next;
}
prev->next = position->next;
free(position)
//position=NULL;
}
- 插入
此处可能会存在某些任意让人疑惑的点
- position的值(地址)从何而来:一般通过Find函数来查找到链表内部的节点地址
- 为何要对position进行断言:若position为NULL,无非两种情况a.未查询到 b.原链表就为NULL.
以a情况来说,在未查询到节点位置时进行插入这一行为本来就是错误的,所以应该不被允许;
以b情况来看,原链表为空,则一开始的查询行为就不该存在,而应该直接使用头插/尾插。当查询行为不存在时,Insert函数自然也不会使用(需要的传参不足);
void Insertnode(SLlist* pHead, SLlist* position, SLlistData x) {//插入数据(找到position地址,插在前面),插入节点的地址position通过Find来查找
assert(pHead);
assert(position);//若为NULL则未查询到,或原链表就为空。
SLlist* node=pHead->next;
if (position == pHead->next)//插为头节点
PushFront(pHead, x);
else{
SLlist* prev = node;
while (node!=position)
{
//此处判断节点相等时,可能会存在某些疑惑,如poition的地址与链表中相同元素的节点的地址是相同的吗
node = node->next;
}
SLlist* newnode = BuySListnode(x);
newnode->next = node;
prev->next = newnode;
)
}
三、源码
1.SLlist.h
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLlistData;//(模拟作为存储数据的结构体)保存数据
typedef struct SLlist {
SLlistData Data;
struct SLlist* next;
}SLlist;
SLlist* InitSLlist(SLlist *node);//初始化头结点,返回值为结构体地址
SLlist* BuySLlistnode(SLlistData x);//创建节点
SLlist* FindSLlistnode(SLlist* pHead,SLlistData x);//查找数据节点
void PushFront(SLlist* pHead, SLlistData x);//头插
void PopFront(SLlist* pHead);//头删
void PopBack(SLlist* pHead);//尾删
void Insertnode(SLlist* pHead, SLlist* position, SLlistData x);//插入数据
void Erasenode(SLlist* pHead, SLlist* position);//删除当前节点
void SLlistprint(SLlist* pHead);//展示
2.SLlist.c
#include"Slist.h"
void SLlistprint(SLlist* pHead) {
SLlist* cur = pHead->next;
while (cur)
{
printf("%d->", cur->Data);
cur = cur->next;
}
printf("NULL\n");
}
SLlist* InitSLlist(SLlist* node) {//初始化头结点,返回值为结构体地址
node = (SLlist*)malloc(sizeof(SLlist));
if (node == NULL)
{
perror("malloc error:");
exit(-1);//开辟失败,终止程序
}
node->next = NULL;
return node;
}
SLlist* BuySLlistnode(SLlistData x) {//创建节点
SLlist* node = (SLlist*)malloc(sizeof(SLlist));
if (node == NULL)
{
perror("malloc error : ");
exit(-1);//开辟失败,终止程序
}
node->Data = x;
node->next = NULL;
return node;
}
SLlist* FindSLlistnode(SLlist* pHead,SLlistData x) {//cur指current
assert(pHead);//因为带表头,而表头不被允许为NULL所以要进行断言
SLlist* cur;
cur = pHead->next;
while (cur)
{
if (cur->Data == x)
return cur;
else
cur = cur->next;
}
return NULL;
}
void PushFront(SLlist* pHead, SLlistData x) {//头插
assert(pHead);
SLlist* node=(SLlist*)malloc(sizeof(SLlist));
if (node == NULL)
{
perror("malloc error:");
exit(-1);//开辟失败,终止程序
}
node = BuySLlistnode(x);//Buy内部为node使用malloc开辟空间了
node->next = pHead->next;
pHead->next = node;
}
void PopFront(SLlist* pHead) {
assert(pHead);//注意此处的pHead指向表头,非真的头结点,不可为NULL
if (pHead->next == NULL)
return;
else
pHead->next = pHead->next->next;
}
void PushBack(SLlist* pHead, SLlistData x) {
assert(pHead);
SLlist* cur=pHead->next;
SLlist* node = BuySLlistnode(x);//创建时next就已经置空
while (cur&&cur->next)//真实的头结点可能为NULL,所以需要考虑到一开始头(head)节点就是尾(tail)节点的情况
{
cur = cur->next;
}
if (NULL == cur)
pHead->next = node;
else
cur->next = node;
}
void PopBack(SLlist* pHead) {
assert(pHead);
SLlist* cur = pHead->next;
SLlist* prev = pHead->next;//真头结点为空时也用不上prev,所以直接赋为头节点
while (cur && cur->next)
{
prev = cur;//保存上一节点
cur = cur->next;
}
if (cur == NULL)
return;
else
prev->next = NULL;
}
void Insertnode(SLlist* pHead, SLlist* position, SLlistData x) {//插入数据(找到position地址,插在前面),插入节点的地址position通过Find来查找
assert(pHead);
assert(position);//若为NULL则未查询到,或原链表就为空。
SLlist* node=pHead->next;
if (position == pHead->next)//插为头节点
PushFront(pHead, x);
else
{
SLlist* prev = node;
while (node != position)
{
//此处判断节点相等时,可能会存在某些疑惑,如poition的地址与链表中相同元素的节点的地址是相同的吗
node = node->next;
}
SLlist* newnode = BuySLlistnode(x);
newnode->next = node;
prev->next = newnode;
}
}
void Erasenode(SLlist* pHead, SLlist* position) {
assert(pHead);
assert(position);
SLlist* cur = pHead->next;
SLlist* prev = pHead->next;
while (cur != position)
{
prev = cur;
cur = cur->next;
}
prev->next = position->next;
free(position)
//position=NULL;
}
结言
带哨兵位的单链表属于最基本的知识,相较于不带哨兵位的单链表在某些方面代码更加简单,但不带表头的链表往往在后面的学习中起着更加重要的作用。所以之后还会写一篇不带表头的单链表与循环的双向链表来进一步加深这方面的学习