TD02-线性表的链式存储-单链表、双链表、循环链表、静态链表
一、单链表
1.单链表基本概念
1、不需要使用地址连续的存储单元
2、通过“链”建立起数据元素之间的逻辑关系,插入和删除操作不需要移动大量元素,只需修改指针
3、每个链表结点,存放自身信息和一个指向后继的指针
4、查找某个特定结点,需要从表头开始遍历,依次查找
2.基本操作
1.单链表结点类型
typedef struct LNode { //typedef关键字--数据类型重命名,如:typedef int zhengshu;
int data; //数据域
struct LNode* next; //指针域
}LNode, * Linklist; //Linklist声明一个指向单链表第一个结点的指针
2.头插法建立单链表
//头插法建立单链表---------------头插法的重要应用:链表的逆置
Linklist List_HeadInsert(Linklist &L) {
LNode* s;
int x;
L = (Linklist)malloc(sizeof(LNode)); //创建头结点
L->next = NULL; //初始为空链表 防止有脏数据,使得L指向一片神秘的区域 养成好习惯,初始化单链表,先把头指针指向NIULL
scanf_s("%d", &x);
while (x != 9999) { //输入9999表示结束
s = (LNode*)malloc(sizeof(LNode)); //增加新结点:在内存中申请一个结点所需空间,并用指针s指向这个空间
s->data = x;
s->next = L->next;
L->next = s; //将新结点插入表中,L为头指针
scanf_s("%d", &x);
}
return L;
}
3.尾插法建立单链表
//尾插法建立单链表----------------------------------时间复杂度O(n)
Linklist List_TailInsert(Linklist& L) {
int x;
L = (Linklist)malloc(sizeof(LNode)); //建立头结点
LNode* s, * r = L; //r为表尾指针
scanf_s("%d", &x);
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s; //r指向新的表尾结点
scanf_s("%d", &x);
}
r->next = NULL; //尾结点指针置空
return L;
}
4.按序号查找结点
//按序号查找结点
LNode* GetElem(Linklist L, int i) { //LNode*:强调这是一个结点; LinkList:强调这是一个单链表,(即使LinkList L等价于LNode* L,增加代码可读性。L通过头结点中指针域顺连着)
if (i == 0)
return L;
if (i < 1)
return NULL;
int j = 1;
LNode* p = L->next;
while (p!=NULL && j < i) { //循环找到第i个结点
p = p->next;
j++;
}
return p;
}
5.按值查找表结点
//按值查找表结点
LNode* LocationElem(Linklist L, int e) {
LNode* p = L->next;
while (p != NULL && p->data!=e)
p = p->next;
return p;
}
6.按位序插入结点(带头结点)
//插入结点操作 按位序插入(带头结点)。(如果不带头结点,要对第一个位置的删除或增加操作单独操作)
bool ListInsert(Linklist& L, int i, int e) {
if (i < 1) //判断插入位置是否合法
return false;
//LNode* p = GetElem(L, i - 1); //可以用封装
LNode* p;
int j = 0;
p = L;
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL) //判断插入位置是否合法,防止i太大(如果一共有四个元素,此时想在第六个位置插入数据,当GetElem循环里j==5时,p==null,即插入位置不合法)
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
//return InsertNextNode(p, e); //可以用封装
}
7.指定结点的后插操作
//指定结点的后插操作
bool InsertNextNode(LNode* p, int e) {
if (p == NULL) //这里的p==NULL判断是为了ListInsert里调用时,会出现GetElem方法中返回的p会有NULL的情况--------提高健壮性
return false;
LNode* s = (LNode*)malloc(sizeof(LNode));
if (s == NULL) //判断内存是否分配失败,如内存不足(不是核心代码)
return false;
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
8.前插操作
//前插操作:在p结点之前插入元素e
//方法1、传入头指针,循环寻找p的前驱结点------时间复杂度O(n)
//方法2、偷天换日-----------------------------时间复杂度O(1)
//这里用的方法2 效果:*s插入*p的前面 实现:首先仍将*s插入到*p的后面,再将p->data和s->data(即e)的值互换---------此时:时间复杂度O(1)
bool InsertPriorNode(LNode* p, int e) {
LNode* s = (LNode*)malloc(sizeof(LNode));
if (p == NULL|| s == NULL)
return false;
s->next = p->next;
p->next = s;
s->data = p->data;
p->data = e;
return true;
}
9.按位序删除
//按位序删除
bool ListDelete(Linklist& L, int i, int& e) {
if (i < 1)
return false;
//LNode* p = GetElem(L, i - 1); //可以用封装
LNode* p;
int j = 0;
p = L;
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL || p->next == NULL)
return false;
LNode* q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
10.删除指定结点
//删除指定结点q----------类似于之前的前插操作
bool DeleteNode(LNode* p) {
if (p == NULL)
return false;
LNode* q = p->next;
p->data = p->next->data; //有bug如果p是最后一个结点,,,不可取这种方法,只能从表头开始依次寻找p的前驱,时间复杂度O(n)
p->next = q->next;
free(q);
return true;
}
11.求表长度
//求表长度
int Length(Linklist L) {
int len = 0;
LNode* p = L;
while (p->next != NULL) {
p = p->next;
len++;
}
return len;
}
12.打印
void print(Linklist L) {
LNode* p = (LNode*)malloc(sizeof(LNode));
p = L->next;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
}
13.主函数
int main(){
Linklist L;
printf("\n头插法创建单链表L1---请输入单链表元素(以9999结束):");
List_HeadInsert(L);
printf("单链表L1: ");
print(L);
printf("\n尾插法建立单链表L2---请输入单链表元素(以9999结束):");
List_TailInsert(L);
printf("单链表L2: ");
print(L);
int findByLocation = GetElem(L, 2)->data;
printf("\n查找L2中位序为2的值:%d", findByLocation);
ListInsert(L, 3, 77);
printf("\n在L2第3个位置按位插入结点77:");
print(L);
int e;
if (ListDelete(L, 3, e))
printf("\n删除L2中位序为3的结点,删除值为:%d", e);
else
printf("\n删除位置不合法");
printf("\n删除后单链表L2:");
print(L);
return 0;
}
实际编程时尽量使用封装,为了方便学习和回顾操作内部逻辑关系,没有选择封装。
3.运行结果
二、双链表
双链表中的按值查找和按位查找与单链表相同,删除、插入不同,因为方便找到前驱结点,所以插入删除操作时间复杂度O(1)
1.双链表的存储结构
//双链表---定义双链表结点类型
typedef struct DNode {
int data;
struct DNode* prior, * next;
}DNode, * DLinkList;
2.双链表的插入操作
s->next = p->next;
p->next->prior = s;
s->prior = p;
p->next = s;
3.双链表的删除操作
p->next = q->next;
q->next->prior = p;
free(p);
三、循环链表
1.循环单链表
1、循环单链表和单链表区别在于:循环单链表中最后一个结点指向的不是NULL,而是指向头结点
2、判空条件:是否等于头指针(p!=L或p->next!=L,不带头结点和带头结点)
3、循环单链表不设头指针,而设尾指针,使得操作效率更高。若头指针,对表尾操作需要O(n);若尾指针为r,r->next即为头指针,对表头表尾的操作都只需要O(1)。
2.循环双链表
带头结点的双循环链表L判断空表条件:
L->prior==L&&L->next==L
四.静态链表
1、静态链表借助数组来描述线性表的链式存储结构
2、结点包含数据域data和指针域next
3、这里的指针是结点的相对地址(数组下标),又称游标
4、和顺序表一样,静态链表也需预先分配一块连续的内存空间
5、带头结点的双循环链表L判断空表条件:
6、静态链表以next==-1作为其结束的标志
7、静态链表插入删除操作与动态链表相同,只需修改指针,不需移动元素
静态链表结构类型描述
//静态链表结构类型描述
#define MaxSize=50
typedef struct {
ElemType data;
int next;
}SLinkList[MaxSize];
五.顺序表和链表的比较
1.逻辑结构
都是线性表
2.存储结构
线性表:
随机存取、存储密度大;
大片连续空间分配不方便,改变容量大小不方便;
链表:
离散的小空间分配方便,改容量方便;
不可随机存取,存储密度低;
3.数据的运算
顺序表:
1、销毁
静态分配:静态数组(销毁时系统自动回收)
动态分配:动态数组(销毁时手动free;malloc和free----创建和销毁----是成对出现)
2、插入和删除
插入或删除都要依次移动后面或前面的数据元素,时间复杂度O(n)
(这里的时间复杂度O(n)主要进行元素移动操作)
3、查找
按位查找:O(1)----随机存取
按值查找:O(n),若表内元素有序,折半查找等O(log2n)
链表:
1、销毁
依次删除各结点(free)
2、插入和删除
插入和删除只需要修改指针,时间复杂度O(n)
(这里的时间复杂度O(n)主要进行元素查找操作。结合考虑现实因素:如果数据元素大,则顺序表的O(n)移动的时间代价大,相比链表查找元素的时间代价更低)
3、查找
按值查找:O(n)
按位查找:O(n)
顺序表和链表的选择三方面:
逻辑结构----都是线性结构****
存储结构----顺序表***链表***
基本操作----初始化、插入、删除、查找***