C语言数据结构:线性表之单链表的实现讲解(带头结点)
目录
前言
链表是线性表的一种,与顺序表不同的是,其虽然在逻辑上相邻,但在实际的物理内存中不相邻。
链表是一种物理储存单元上非连续、非顺序的储存结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
链表最明显的好处就是,常规 数组 排列关联项目的方式可能不同于这些数据项目在 记忆体 或 磁盘 上顺序,数据的存取往往要在不同的排列顺序中转换。 链表允许插入和移除表上任意位置上的节点 ,但是不允许随机存取 。
本篇文章将针对一般的单向链表进行讲解及代码实现,其他种类链表将在后续文章实现
一、带头结点的单链表
如图所示,带头结点的单链表由两部分构成:
1.不存储数据的头结点(表头)
2.保存数据的链表体
这种结构使链表的操作更加简单,对数据的调用更加方便快捷,我们在调用链表时只需要传递表头信息即可
1.带头结点单链表头文件及函数声明
新建头文件"list.h"对链表函数声明进行保存,方便我们后期查看及使用
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
typedef int ELEM_TYPE;
//有效数据节点结构体设计:
typedef struct Node
{
ELEM_TYPE data;//数据域
struct Node* next;//指针域
}Node, *PNode;
//购买结点
PNode BuyNode();
//初始化
void Init_list(PNode p);
//头插
bool Insert_head(PNode p, ELEM_TYPE val);
//尾插
bool Insert_tail(PNode p, ELEM_TYPE val);
//按位置插
bool Insert_pos(PNode p, int pos, ELEM_TYPE val);
//头删
bool Del_head(PNode p);
//尾删
bool Del_tail(PNode p);
//按位置删//pos==0(删除第一个有效值)
bool Del_pos(PNode p, int pos);
//按值删
bool Del_val(PNode p, ELEM_TYPE val);
//查找
struct Node* Search(PNode p, ELEM_TYPE val);
//查找 前置结点
struct Node* Search_Prev(PNode p, ELEM_TYPE val);
//判空
bool IsEmpty(PNode p);
//判满 因为是单链表 节点都是用一个申请一个 所以不需要判满
//获取单链表有效值个数
int Get_length(PNode p);
//打印链表
void Show(PNode p);
//清空 节点清空时,直接释放内存,用的时候再申请
void Clear(PNode p);
//销毁
void Destroy(PNode p);
2.初始化
在对使用链表进行数据的储存之前,我们需要进行链表的初始化,首先需要对链表的基础结构进行构建。
链表结点由两个部分构成:数据域与指针域
数据域进行数据保存,指针域指向下一个结点的地址,若不存在下一个结点,那么指针域指向NULL即可。
结点结构的设计如下:
typedef int ELEM_TYPE;//如此对数据类型进行改名操作,若要进行修改数据类型操作会更加方便
//有效数据节点结构体设计:
typedef struct Node
{
ELEM_TYPE data;//数据域
struct Node* next;//指针域
}Node, *PNode;
接着对链表初始化函数进行实现,这里的实现较为简单,直接将表头的指针域置为NULL即可,因为表中尚无元素。
//初始化
void Init_list(PNode p)
{
assert(p != NULL);
p->next = NULL;
}
3.结点申请
因为链表的结构特性,我们需要对结点进行多次申请,为了方便进行其他函数操作,并且为了优化代码,我们将结点的申请封装于一个函数
PNode BuyNode()
{
PNode newnode = (PNode)malloc(sizeof(Node));
if (newnode == NULL)//判断申请的结点是否有效
{
exit(0);
}
memset(newnode, 0, sizeof(Node));//对申请内存置空
return newnode;
}
4.数据插入
为了减少代码的重复和冗余,我们先进行按位置插入函数的实现,其他的插入就可直接调用
1.按位置插入
//按位置插
bool Insert_pos(PNode p, int pos, ELEM_TYPE val)
{
assert(p != NULL);
if (pos < 0 || pos > Get_length(p))//判断插入位置是否合法
{
return false;
}
PNode node = p;
for (int i = 0; i < pos; i++)//将指针置于需要插入的位置
{
node = node->next;
}
PNode newnode = BuyNode();
newnode->data = val;
newnode->next = node->next;
node->next = newnode;
return true;
}
2.头插
直接调用按位置插入,插入位置为0
//头插
bool Insert_head(PNode p, ELEM_TYPE val)
{
assert(p != NULL);
return Insert_pos(p, 0, val);
}
3.尾插
调用链表长度获取函数,再将链表元素数量信息,作为插入位置传给按位插入函数
//尾插
bool Insert_tail(PNode p, ELEM_TYPE val)
{
assert(p != NULL);
return Insert_pos(p, Get_length(p), val);
}
5.查找
我们先进行查找某值前置结点的函数实现,如此可方便其他函数调用,减少代码冗余。
1.查找前置结点
在为未查找到值的情况下,返回尾结点指针,这样可以防止其余函数调用时出现空指向问题
//查找 前置结点
struct Node* Search_Prev(PNode p, ELEM_TYPE val)
{
assert(p != NULL);
PNode node = p;
while (node->next != NULL && node->next->data != val)
{
node = node->next;
}
return node;
}
2.查找值结点
直接调用查找1即可,且不用担心出现空指向问题
//查找
struct Node* Search(PNode p, ELEM_TYPE val)
{
assert(p != NULL);
return Search_Prev(p, val)->next;
}
6.数据删除
与前面插入函数相同,我们先实现按位置删除函数
1.按位置删除
//按位置删//pos==0(删除第一个有效值)
bool Del_pos(PNode p, int pos)
{
assert(p != NULL);
if (pos < 0 || pos >= Get_length(p))//判断位置是否合法
{
return false;
}
PNode node = p;
for (int i = 0; i < pos; i++)
{
node = node->next;
}
PNode delnode = node->next;//将要删除的结点独立出来
node->next = delnode->next;
free(delnode);//释放要删除的结点
delnode = NULL;
return true;
}
2.头删
在调用按位置删除函数前进行判空操作,可能减少函数调用
//头删
bool Del_head(PNode p)
{
assert(p != NULL);
if (IsEmpty(p))
{
return false;
}
return Del_pos(p, 0);
}
3.尾删
//尾删
bool Del_tail(PNode p)
{
assert(p != NULL);
if (IsEmpty(p))
{
return false;
}
return Del_pos(p, Get_length(p) - 1);
}
4.按值删除
调用了查找前置函数
//按值删
bool Del_val(PNode p, ELEM_TYPE val)
{
assert(p != NULL);
if (IsEmpty(p))
{
return false;
}
PNode node = Search_Prev(p, val);
PNode delnode = node->next;
if (delnode != NULL)//判断需要删除的结点是否为空
{
node->next = delnode->next;
free(delnode);
delnode = NULL;
}
return true;
}
7.清空与销毁
1.清空
这里我们使用比较简单的方法,一直头删即可
//清空 节点清空时,直接释放内存,用的时候再申请
void Clear(PNode p)
{
assert(p != NULL);
while (!IsEmpty(p))
{
Del_head(p);
}
}
2.销毁
调用清空函数
//销毁
void Destroy(PNode p)
{
Clear(p);
p->next = NULL;
}
8.带头结点单链表源文件及整体函数实现
#include "list.h"
//初始化
void Init_list(PNode p)
{
assert(p != NULL);
p->next = NULL;
}
PNode BuyNode()
{
PNode newnode = (PNode)malloc(sizeof(Node));
if (newnode == NULL)
{
exit(0);
}
memset(newnode, 0, sizeof(Node));
return newnode;
}
//头插
bool Insert_head(PNode p, ELEM_TYPE val)
{
assert(p != NULL);
return Insert_pos(p, 0, val);
}
//尾插
bool Insert_tail(PNode p, ELEM_TYPE val)
{
assert(p != NULL);
return Insert_pos(p, Get_length(p), val);
}
//按位置插
bool Insert_pos(PNode p, int pos, ELEM_TYPE val)
{
assert(p != NULL);
if (pos < 0 || pos > Get_length(p))
{
return false;
}
PNode node = p;
for (int i = 0; i < pos; i++)
{
node = node->next;
}
PNode newnode = BuyNode();
newnode->data = val;
newnode->next = node->next;
node->next = newnode;
return true;
}
//头删
bool Del_head(PNode p)
{
assert(p != NULL);
if (IsEmpty(p))
{
return false;
}
return Del_pos(p, 0);
}
//尾删
bool Del_tail(PNode p)
{
assert(p != NULL);
if (IsEmpty(p))
{
return false;
}
return Del_pos(p, Get_length(p) - 1);
}
//按位置删//pos==0(删除第一个有效值)
bool Del_pos(PNode p, int pos)
{
assert(p != NULL);
if (pos < 0 || pos >= Get_length(p))
{
return false;
}
PNode node = p;
for (int i = 0; i < pos; i++)
{
node = node->next;
}
PNode delnode = node->next;
node->next = delnode->next;
free(delnode);
delnode = NULL;
return true;
}
//按值删
bool Del_val(PNode p, ELEM_TYPE val)
{
assert(p != NULL);
if (IsEmpty(p))
{
return false;
}
PNode node = Search_Prev(p, val);
PNode delnode = node->next;
if (delnode != NULL)
{
node->next = delnode->next;
free(delnode);
delnode = NULL;
}
return true;
}
//查找
struct Node* Search(PNode p, ELEM_TYPE val)
{
assert(p != NULL);
return Search_Prev(p, val)->next;
}
//查找 前置结点
struct Node* Search_Prev(PNode p, ELEM_TYPE val)
{
assert(p != NULL);
PNode node = p;
while (node->next != NULL && node->next->data != val)
{
node = node->next;
}
return node;
}
//判空
bool IsEmpty(PNode p)
{
assert(p != NULL);
return p->next == NULL;
}
//判满 因为是单链表 节点都是用一个申请一个 所以不需要判满
//获取单链表有效值个数
int Get_length(PNode p)
{
assert(p != NULL);
int count = 0;
PNode node = p;
while (node->next != NULL)
{
node = node->next;
count++;
}
return count;
}
//打印链表
void Show(PNode p)
{
assert(p != NULL);
PNode node = p->next;
while (node != NULL)
{
printf("%d ", node->data);
node = node->next;
}
printf("\n");
}
//清空 节点清空时,直接释放内存,用的时候再申请
void Clear(PNode p)
{
assert(p != NULL);
while (!IsEmpty(p))
{
Del_head(p);
}
}
//销毁
void Destroy(PNode p)
{
Clear(p);
p->next = NULL;
}