这是数据结构入门系列笔记的第二篇。
前言
上一章,我们学习了顺序表,了解到了顺序表的相关特性。
顺序表的特点: 以物理位置相邻表示逻辑关系。顺序表的有点: 任一元素均可随机存取。顺序表的缺点: 进行插入和删除操作时。需要移动大量的元素。存储空间不灵活。
为了应对顺序存储结构的缺点,我们引入今天需要学习的链式存储结构。
链表的基础知识
什么是链式存储结构
链表是指用一组物理位置任意的存储单元来存放线性表的数据元素。
这组存储单元既可以是连续的,也可以是不连续的,甚至是零散的分布在内存中的任意位置上。
如图所示,链表是由n个节点组成的,其中的各个节点又是由两个部分组成的,一个是 数据域,一个是 指针域:
- 数据域:存储元素数值数据
- 指针域:存储直接后继节点的存储位置
链表的分类
链表的种类有很多种,大体可以分为三种:
- 单链表:节点只有一个指针域的链表,称为
单链表或者线性链表。
- 双链表:节点有两个指针域的链表,称为
双链表。
- 循环链表:首尾相接的链表称为
循环链表。
当然,链表的分类还有很多,例如带头节点或不带头节点的链表,双向或不双向的链表等等,排列组合下来,链表的种类也是非常的多。
链表的特点
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
- 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后
顺序扫描其余节点,所以寻找到第一个结点和最后一个结点所花费的时间不等
单链表的实现
线性表的定义
#include "stdio.h"
#include "string.h"
#include "ctype.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
typedef struct Node
{
ElemType data;
struct Node *next;
}Node,*LinkList;
注意,这里定义了两种节点的定义方式
Node *L和LinkList L,且这两种方式完全等价。当然,我们一般使用后面一种。
单链表的初始化
- 生成新结点作头结点,用头指针 L 指向头结点。
- 将头结点的指针域置空
Status InitList(LinkList &L)
{
L=(LinkList)malloc(sizeof(Node)); /* 产生头结点,并使L指向此头结点 */
if(!L) /* 存储分配失败 */
return ERROR;
L->next=NULL; /* 指针域为空 */
return OK;
}
判断链表为空
判断一个链表是否为空,只需要判断这个链表的头结点的指针域是否为空。
Status ListEmpty(LinkList L)
{
if(L->next)
return FALSE;
else
return TRUE;
}
上面代码如果为空,则返回FALSE,否则返回TRUE
销毁单链表
从头指针开始,依次释放所有节点。
Status DestoryList(LinkList &L){
LinkList p;
while(L){
p = L;
L = L->next;
free(p);
}
return OK;
}
清空单链表
链表仍然存在,但是链表中无元素,成为空链表(头指针和头节点仍然存在)。
清空链表相差不大,只不过要保留头部的L,那么我们就需要另外两个指针 p 和 q。剩下的操作就和销毁链表的操作一致啦。
Status ClearList(LinkList &L)
{
LinkList p,q;
p=L->next; /* p指向第一个结点 */
while(p) /* 没到表尾 */
{
q=p->next;
free(p);
p=q;
}
L->next=NULL; /* 头结点指针域为空 */
return OK;
}
返回链表中的元素个数
int ListLength(LinkList L)
{
int i=0;
LinkList p=L->next; /* p指向第一个结点 */
while(p)
{
i++;
p=p->next;
}
return i;
}
获取链表的第 i 个元素
- 从第1个节点(L->next)顺链扫描,用指针 p 指向当前扫描到的结点,p 初值 p = L->next。
- j 做计数器,累计当前扫描过的结点数,j 初值为 1 。
- 当 p 指向扫描到的下一结点时,计数器 j 加 1。
Status GetElem(LinkList L,int i,ElemType &e)
{
int j;
LinkList p; /* 声明一结点p */
p = L->next; /* 让p指向链表L的第一个结点 */
j = 1; /* j为计数器 */
while (p && j<i) /* p不为空或者计数器j还没有等于i时,循环继续 */
{
p = p->next; /* 让p指向下一个结点 */
++j;
}
if ( !p || j>i )
return ERROR; /* 第i个元素不存在 */
e = p->data; /* 取第i个元素的数据 */
return OK;
}
循环结束的条件是,当前
p为空或者计数器j大于i时,前者是因为传入想查找的值大于链表本身的长度,后者是因为传入的值是0/-1等等。
查找元素
- 从第一个结点起,依次和e相比较。
- 如果找到一个值与e相等的数据元素,则返回其在链表中的 “位置” 或 地址。
- 如果查遍整个链表都没有找到和e相等的元素,则返回0或“NULL”。
int LocateElem(LinkList L,ElemType e)
{
int i=0;
LinkList p=L->next;
while(p)
{
i++;
if(p->data==e) /* 找到这样的数据元素 */
return i;
p=p->next;
}
return 0;
}
插入新结点
- 首先找到 Ai-1 的存储位置 p。
- 生成一个数据域为 e 的新结点 s。
- 插入新结点:1.新结点的指针域指向结点 Ai 2. 结点 Ai-1的指针域指向新结点
- s -> next = p -> next;
- p -> next = s;
注意:这两步是有顺序之分的,需要格外小心。
Status ListInsert(LinkList &L,int i,ElemType e)
{
int j;
LinkList p,s;
p = L;
j = 1;
while (p && j < i) /* 寻找第i个结点 */
{
p = p->next;
++j;
}
if (!p || j > i)
return ERROR; /* 第i个元素不存在 */
s = (LinkList)malloc(sizeof(Node)); /* 生成新结点(C语言标准函数) */
s->data = e;
s->next = p->next; /* 将p的后继结点赋值给s的后继 */
p->next = s; /* 将s赋值给p的后继 */
return OK;
}
删除结点
- 首先找到 Ai-1 的储存位置p,保存要删除的a的值。
- 令 p->next 指向 Ai+1 。
- 释放点 Ai 结点
p->next = p->next->next
Status ListDelete(LinkList &L,int i,ElemType e)
{
int j;
LinkList p,q;
p = L;
j = 1;
while (p->next && j < i) /* 遍历寻找第i个元素 */
{
p = p->next;
++j;
}
if (!(p->next) || j > i)
return ERROR; /* 第i个元素不存在 */
q = p->next;
p->next = q->next; /* 将q的后继赋值给p的后继 */
e = q->data; /* 将q结点中的数据给e */
free(q); /* 让系统回收此结点,释放内存 */
return OK;
}
头插法建立链表
- 从一个空表开始,重复读入数据
- 生成新结点,将读入数据存放到新结点的数据域中
- 从最后一个结点开始,依次将各结点插入到链表的前端
void CreateListHead(LinkList &L, int n)
{
LinkList p;
int i;
srand(time(0)); /* 初始化随机数种子 */
L = (LinkList)malloc(sizeof(Node));
L->next = NULL; /* 先建立一个带头结点的单链表 */
for (i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */
p->data = rand()%100+1; /* 随机生成100以内的数字 */
p->next = L -> next;
L->next = p; /* 插入到表头 */
}
}
尾插法建立链表
- 从一个空表L开始,将结点逐个插入到链表的尾部,尾指针r指向链表的尾结点
- 初始时,r同L均指向头结点,每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点
void CreateListTail(LinkList &L, int n)
{
LinkList p,r;
int i;
srand(time(0)); /* 初始化随机数种子 */
L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */
r=L; /* r为指向尾部的结点 */
for (i=0; i<n; i++)
{
p = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */
p->data = rand()%100+1; /* 随机生成100以内的数字 */
r->next=p; /* 将表尾终端结点的指针指向新结点 */
r = p; /* 将当前的新结点定义为表尾终端结点 */
}
r->next = NULL; /* 表示当前链表结束 */
}
输出链表中的所有元素
// 访问元素
Status visit(ElemType c)
{
printf("%d ",c);
return OK;
}
// 输出链表中的所有元素
Status ListTraverse(LinkList L)
{
LinkList p=L->next;
while(p)
{
visit(p->data);
p=p->next;
}
printf("\n");
return OK;
}
检测
int main()
{
LinkList L;
ElemType e;
Status i;
int j,k;
i=InitList(L);
printf("初始化L后:ListLength(L)=%d\n",ListLength(L));
for(j=1;j<=5;j++)
i=ListInsert(L,1,j);
printf("在L的表头依次插入1~5后:L.data=");
ListTraverse(L);
printf("ListLength(L)=%d \n",ListLength(L));
i=ListEmpty(L);
printf("L是否空:i=%d(1:是 0:否)\n",i);
i=ClearList(L);
printf("清空L后:ListLength(L)=%d\n",ListLength(L));
i=ListEmpty(L);
printf("L是否空:i=%d(1:是 0:否)\n",i);
for(j=1;j<=10;j++)
ListInsert(L,j,j);
printf("在L的表尾依次插入1~10后:L.data=");
ListTraverse(L);
printf("ListLength(L)=%d \n",ListLength(L));
ListInsert(L,1,0);
printf("在L的表头插入0后:L.data=");
ListTraverse(L);
printf("ListLength(L)=%d \n",ListLength(L));
GetElem(L,5,e);
printf("第5个元素的值为:%d\n",e);
for(j=3;j<=4;j++)
{
k=LocateElem(L,j);
if(k)
printf("第%d个元素的值为%d\n",k,j);
else
printf("没有值为%d的元素\n",j);
}
k=ListLength(L); /* k为表长 */
for(j=k+1;j>=k;j--)
{
i=ListDelete(L,j,e); /* 删除第j个数据 */
if(i==ERROR)
printf("删除第%d个数据失败\n",j);
else
printf("删除第%d个的元素值为:%d\n",j,e);
}
printf("依次输出L的元素:");
ListTraverse(L);
j=5;
ListDelete(L,j,e); /* 删除第5个数据 */
printf("删除第%d个的元素值为:%d\n",j,e);
printf("依次输出L的元素:");
ListTraverse(L);
i=ClearList(L);
printf("\n清空L后:ListLength(L)=%d\n",ListLength(L));
CreateListHead(L,20);
printf("整体创建L的元素(头插法):");
ListTraverse(L);
i=ClearList(L);
printf("\n删除L后:ListLength(L)=%d\n",ListLength(L));
CreateListTail(L,20);
printf("整体创建L的元素(尾插法):");
ListTraverse(L);
return 0;
}
参考文献及资料
(100条消息、) 【数据结构】链表(单链表实现+详解+原码)_风继续吹TT的博客-CSDN博客
结语
好啦,本次分享就到这里。
文章如果有不正确的地方,欢迎指正,共同学习,共同进步。
若有侵权,请联系作者删除。