数据结构之单链表
线性表的顺序存储结构在实现数据元素的插入和删除时需要移动大量的数据元素,影响算法效率。而且如果初始空间分配不当还会造成空间浪费。
与顺序存储结构不同,链式存储结构将数据元素的存储空间映射为由数据域和指针域组成的结点。其中数据域用于存储数据元素,而数据元素之间的逻辑关系通过指针域来建立。通常,在指针域中存储的是一个指向其他结点的指针,该指针也称作链。
1.线性链表的结构
线性链表也称单链表,是线性表最简单的一种链式存储结构。线性表中的每个结点除数据域外,只包含一个指向其后继的指针域。结点如图所示
其中data域用于存储数据元素,next域是指向本结点直接后继的指针。结点结构可以表示如下
typedef int ElemType // 定义元素类型为int型
typedef struct node{
ElemType data; // 数据域
struct node *next; // 指针域
}ListNode,*LinkList; // ListNode为表结点;LinkList为指向结点的指针,表示一个单链表
线性链表的存取必须从表头指针开始,表头指针指示线性链表中的第一个结点的存储位置。由于线性链表的最后一个数据元素没有直接后继,因此线性链表中的最后一个结点的指针为“空”(NULL)。有时为了使用方便,通常在表的第一个结点的前面增加一个被称为头结点的附加结点,该结点的数据域可以为空,也可以存储诸如表长度等附加信息,指针域中存储指向单链表第一个结点。由于头结点的引入,使线性表的头指针总是不空,这给链表的插入、删除等操作带来了方便。
下面以带头结点的单链表作为存储结构,描述线性表的主要基本操作的实现算法。
2.单链表基本操作的实现
通常情况下,需要了解一些操作的执行状态(例如成功、失败等),为此引入一个抽象数据类型 Status
,用其返回操作的执行状态。例如:
#define OK 0 // 成功执行
#define Err_Memory -1 // 内存非配错误
#define Err_InvalidParam -2 // 输入参数无效
#define Err_Overflow -3 // 溢出错误
#define Err_IllegalPos -4 // 非法位置
#define Err_NoResult -5 // 无返回结果或返回结果为空
上述常量定义语句可以放在顺序表结构定义语句之前,同时用如下语句将 Status
实例化为整型
typedef int Status;
1.初始化单链表
初始化单链表就是构造一个包含头结点且长度为0 的单链表,并把头结点的指针域置为空。
Status InitList(LinkList L){
if(!L)
return Err_InvalidParam; // 链表无效
L->next = NULL; // 头结点指针域为空
return OK;
}
2.情况单链表
清空单链表意味着把除头结点外的所有结点空间释放,并将头结点的指针域置为空。
Status ClearList(LinkList L){
if(!L)
return Err_InvalidParam;
ListNode *p,*q;
p = L->next; // p指向第一个结点
while(p){ // 从第一个结点开始一次释放结点空间
q = p;
p = p->next; // p指向下一结点
free(q); // 释放q的空间
}
L->next = NULL; // 将头结点指针域置空
return OK;
}
3.测试单链表是否为空
对于待头结点的单链表而言,只需判断头结点的指针域是否为空即可。
int EmptyList(LinkList L){
if(!L)
return Err_InvalidParam;
return (L->next == NULL); // 如果L->next为NULL返回True,否则返回False
}
4.求单链表的长度
对于单链表而言,如果在头结点中可以存储线性表的长度,则需要从第一个结点开始计数,直到表尾结点
int LengthList(LinkList L){
int count = 0;
ListNode *p;
p = L->next; // p指向第一个结点
while(p){
p = p->next;
count++;
}
return count;
}
5.遍历单链表
对于带头结点的单链表,只需从第一个结点开始,通过指针一以此访问链表中的所有节点一次即可。
Status TraverseList(LinkList L){
if(!L)
return Err_InvalidParam;
ListNode *p;
p = L->next;
while(p){
printf("%d\t",p->data);
p = p->next;
}
}
6.向线性表中插入元素
与顺序表不同,在单链表中(表长为n)的第 i (1 ≤ i ≤ n+1)个位置上插入元素时,不需要移动元素。
基本过程:
- 找到第i个结点的前驱结点,将指针p指向该结点。包含数据元素e的结点将链接到此结点及其后继结点之间
- 产生一个新结点s,并将数据元素e存放到该结点的数据源中
- 先将新节点s的指针指向p的后继结点,即
s->next = p->next
;再将p的指针指向s,即p->next = s
Status InsertList(LinkList L, int i, ElemType e){
if(!L)
return Err_InvalidParam;
ListNode *p,*s;
p = L;
int j = 1;
while(j<i && p){ // 找到插入位置的前驱结点
p = p->next;
}
if(j>i || !p)
return Err_IllegalPos;
s = (ListNode*)malloc(sizeof(ListNode));
if(!s)
return Err_Memory;
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
该算法时间复杂度为O(n)。
7.删除元素
在单链表存储结构中,删除线性表指定位置的数据元素也不需要移动元素,只需在单链表中把指定位置的结点从链表中摘除,并释放该结点的空间即可。
Status DeleteList(LinkList L, int i, ElemType *e){ // 形参有指针类型ElemType *e
if(!L)
return Err_InvalidParam;
ListNode *p,*q;
p = L;
int j = 1;
while(j<i && p){ // 找到删除位置的前驱结点
p = p->next;
}
if(j>i || !p)
return Err_IllegalPos;
q = p->next; // 将q的指针指向p的后继结点
*e = q->data; // 将删除结点的数据元素保存至e
p->next = q->next;
free(q);
return OK;
}
其中ElemType *e
的目的是传入指针类型数据,将e指向被删除的数据元素,从而在主调函数和被调函数之间传递数据。
8.定位元素
在单链表中查找给定元素e第一次出现的位置,只需要从第一个结点开始,以此将结点的数据域与e相比较,直到找到一个数据域与e相等的结点,返回该结点的位置;或者知道链表结束,也没有找到数据域与e相等的结点,则返回0,表示没有找到数据元素。
int LocateList(ElemType e, LinkList L){
if(!L)
return Err_InvalidParam;
int i = 1;
ListNode *p;
p = L->next;
while(p ** p->data!=e){ // 从第一个结点开始以此与e比较
i++;
p = p->next;
}
if(!p) // 查找失败返回0
return 0;
return i;
}
9.获取元素
在单链表中获取第 i (1 ≤ i ≤ n+1)个位置的数据元素,需要找到第i个结点,并返回该结点的数据域即可。
Status GetElem(LinkList L, int i, ElemType *e){
if(!L)
return Err_InvalidParam;
ListNode *p;
int j = 1;
p = L->next;
while(j<i && p){
i++;
p = p->next;
}
if(j>i || !p)
return Err_IllegalPos;
*e = p->data; // 将第i个位置的数据元素保存到e
return OK;
}
10.创建单链表
当创建含有多个数据元素的单链表时,可以通过多次调用插入算法在表首(第i个位置)依次插入结点,但此时单链表结点的次序与插入次序呈现逆序关系(即先插入的结点为于链表的尾部)。下面介绍一个简历具有n个结点的单链表的算法。为了保证插入结点的顺序与结点在链表中的顺序一致,该算法需要维护一个指向表尾的指针,每次通过该指针在表尾插入结点。
Status CreateLinkList(LinkList L, int n){
if(!L)
return Err_InvalidParam;
ListNode *p,*s;
int i;
p = L;
for(i=0; i<n; i++){
s = (ListNode*)malloc(sizeof(ListNode));// 生成新结点
scanf("%d",&s->data);
s->next = NULL; // 新结点指针域为空
p->next = s; // 将p的指针指向s(s成为p的后继)
p = s; // p指向新结点s
}
return OK;
}
11.销毁链表
销毁链表操作就是删除链表所有结点并释放所分配的存储空间。算法需要从头结点开始,逐一释放所有结点的空间。
Status DestroyLinkList(LinkList L){
if(!L)
return Err_InvalidParam;
ListNode *p;
while(L){
p = L; // p指向要释放的结点
L = L->next; // L指向下一个要释放的结点
free(p);
}
return OK;
}
以上就是c语言实现单链表结构及有关操作的代码