目录
线性表有顺序存储和链式存储两种方式,本文主要是对链式存储进行回顾总结,有关顺序存储可以看这篇文章:顺序表知识总结及代码实现
了解链表前首先应该了解头指针、头结点等知识,下面来依次了解相关知识:
头指针、头结点和首元结点的概念
- 头指针: 是指向链表中第一个结点的指针
- 首元结点: 是指链表中存储第一个数据元素
的结点
- 头结点: 是在链表的首元结点之前附设的一个结点;
链表的概念
定义:链表,别名链式存储结构或单链表,用于存储逻辑关系为 "一对一" 的数据。与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。
链表中每个数据的存储都由以下两部分组成:
- 数据元素本身,其所在的区域称为数据域;
- 指向直接后继元素的指针,所在的区域称为指针域;
即链表中存储各数据元素的结构如下图:
上图所示的结构在链表中称为节点。也就是说,链表实际存储的是一个一个的节点,真正的数据元素包含在这些节点中,如下图:
因此,链表中每个节点的具体实现,需要使用 C 语言中的结构体,具体实现代码为:
typedef struct Lnode{
ElemType data; //代表数据域,ElemType是元素类型,可以按要求更改
struct Lnode *next; //代表指针域,指向后继元素
}Lnode, *LinkList; //Lnode为节点名,每个节点都是一个 Lnode 结构体
提示,由于指针域中的指针要指向的也是一个节点,因此要声明为 Lnode 类型(这里要写成 struct Lnode*
的形式)。
链表的特性
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
- 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等
有关链表的三个讨论 (有助于更好的理解)
➢如何表示空表?
- 无头结点时,头指针为空时表示空表
- 有头结点时,当头结点的指针域为空时表示空表
➢在链表中设置头结点有什么好处?
- 便于首元结点的处理:首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致, 无须进行特殊处理;
- 便于空表和非空表的统一处理:无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了 。
➢头结点的数据域内装的是什么?
头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。
链表功能函数
void InitList(LinkList& L); //1.初始化顺序表
bool Empty(LinkList L); //2.判断单链表是否为空
int GetLength(LinkList L); //3.获取单链表长度
Lnode *GetElem(LinkList L, int i); //4-1.按位查找-查找第i个结点
Lnode *GetLNode(LinkList L, int e); //4-2.按值查找-查找数据域为e的结点
void CreateList_H(LinkList &L, int n); //5.头插法建立单链表
void CreateList_T(LinkList &L, int n); //6.尾插法建立单链表
bool InsertPriorNode(Lnode *p, int e); //7.前插操作-在p结点之前插入数据e
bool InsertNextNode(Lnode *p, int e); //8.后插操作-在p结点之后插入数据e
bool InserstList(LinkList &L, int i, int e); //9.按位序插入
bool DeleteNextDNode(Lnode *p); //10.删除p结点的后继结点
bool DeleteNode(Lnode *p); //11.删除指定结点
bool ListDelte(LinkList &L, int i, int &e); //12.按位序删除
void PrintList(LinkList &L); //13.遍历单链表
实现代码
#include<bits/stdc++.h>
using namespace std;
#define ElemType int //元素类型定义为int
typedef struct Lnode{
ElemType data; //代表数据域,ElemType是元素类型,可以按要求更改
struct Lnode *next; //代表指针域,指向后继元素
}Lnode, *LinkList; //Lnode为节点名,每个节点都是一个 Lnode 结构体
// 初始化单链表
void InitList(LinkList& L) {
L = new Lnode; // L = (Lnode *)malloc(sizeof(Lnode));
L->next = NULL;
}
// 判断单链表是否为空
bool Empty(LinkList L) {
if(L->next != NULL)
return false;
else
return true;
}
// 获取单链表长度
int GetLength(LinkList L) {
Lnode *p = L->next;
int n = 0;
while(p != NULL) {
p = p->next;
n++;
}
return n;
}
// 按位查找:查找第i个结点
Lnode *GetElem(LinkList L, int i) {
if(i < 0) //i不合法
return NULL;
int j = 0;
Lnode *p = L;
while (p != NULL && j < i) {
p = p->next;
j++;
}
return p;
}
// 按值查找:查找数据域为e的结点
Lnode *GetLNode(LinkList L, int e) {
Lnode *p = L->next;
while (p != NULL && L->data != e) {
p = p->next;
}
return p;
}
// 头插法建立单链表
void CreateList_H(LinkList &L, int n) {
L = new Lnode;
L->next = NULL;
while(n--) {
Lnode *p = new Lnode;
cin >> p->data;
p->next = L->next;
L->next = p;
}
}
// 尾插法建立单链表
void CreateList_T(LinkList &L, int n) {
L = new Lnode;
L->next = NULL;
Lnode *r = L;
while(n--) {
Lnode *p = new Lnode;
cin >> p->data;
p->next = r->next;
r->next = p;
r = p;
}
}
// 前插操作:在p结点之前插入数据e
bool InsertPriorNode(Lnode *p, int e) {
if (p == NULL) {
return false;
}
Lnode *s = new Lnode;
s->next = p->next;
s->data = p->data; // 数据后移,模拟结点后移
p->next = s;
p->data = e; // 将前结点置为新插入的结点
return true;
}
// 后插操作:在p结点之后插入数据e
bool InsertNextNode(Lnode *p, int e) {
if (p == NULL) {
return false;
}
Lnode *q = new Lnode;
q->data = e;
q->next = p->next;
p->next = q;
return true;
}
// 按位序插入
bool InserstList(LinkList &L, int i, int e) {
if (i < 1) { // i值不合法
return false;
}
Lnode *p = GetElem(L, i - 1); // 遍历查找i-1个结点
InsertNextNode(p, 5244); // 使用后插法
/* // 使用前插法,达到同样效果
Lnode *p = GetElem(L, i);
InsertPriorNode(p, 5244);
*/
return true;
}
// 删除p结点的后继结点
bool DeleteNextDNode(Lnode *p) {
if (p == NULL || p->next == NULL) {
return false;
}
Lnode *s = new Lnode;
s = p->next;
p->next = s->next;
delete s;
return true;
}
// 删除指定结点
bool DeleteNode(Lnode *p) {
if (p == NULL) {
return false;
}
Lnode *s = new Lnode;
s = p->next; // q指向被删除结点
p->data = s->data; // 数据前移,模拟结点前移
p->next = s->next; // 断开与被删除结点的联系
delete s;
return true;
}
// 按位序删除
bool ListDelte(LinkList &L, int i, int &e) {
if (i < 1) {
return false;
}
/* // 按结点删除,实现同样效果
Lnode *p = GetElem(L, i); // 被删除结点
e = p->data;
DeleteNode(p);
*/
Lnode *p = GetElem(L, i - 1);
e = p->next->data;
DeleteNextDNode(p); // 删除前一结点的后继结点
return true;
}
// 遍历单链表
void PrintList(LinkList &L) {
if (L->next == NULL) {
return;
}
Lnode *p = L->next; // 指向头指针
while (p != NULL) {
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
int main() {
LinkList L;
InitList(L);
int n;
cin >> n;
CreateList_T(L, n); // 尾插法
//CreateList_H(L, n); // 头插法
PrintList(L);
InserstList(L, 1, 5244);
PrintList(L);
int e = -1;
ListDelte(L, 3, e);
cout << "被删除的值:" << e << endl;
PrintList(L);
cout << "长度:" << GetLength(L) << endl;
return 0;
}
链表优缺点
- 优点:
- 结点空间可以动态申请和释放
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素
- 缺点:
- 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大
- 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度