前言
本文的知识点来源:数据结构与算法——线性表(链表篇)_线性链表-CSDN博客,但是这篇博客不能细看,也不能深究,因为错误和纰漏太多了,包括但不限于变量名的错误,函数传参的误导性,以及部分知识点错的离谱,本文也只是跟着这篇博客整理的知识点的结构来自己写的代码,毕竟:Talk is cheap, show me the code.
ps:全文包括代码以及注释都是手写,创作不易,点个赞呗老弟;)
需要你事先掌握的知识点
务必明白c语言指针操作的两个符号:解引用符:*和取地址符:&,本文在代码部分也有讲解,但是没有特别详细的介绍,而且链表本身就是靠指针来操作的,所以对指针的理解应该不至于一无所知。
首先得知道LinkList L, LinkList *L, LinkList &L, LNode *L, LNode **L, 分别代表的什么,不知道的话看这篇:数据结构基础--指针,结构体,typedef,形参实参详解、区分_typedef 指针 结构体-CSDN博客
线性表部分
两种定义
//1.
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList
//2.
//这里的typedef是取别名的意思
struct Lnode{
ElemType data;
struct LNode *next;
};
typedef struct LNode LNode;//把 struct LNode取个简单的名字为 LNode
typedef struct LNode *LinkList;//把struct LNode *取个简单的名字为 LinkList
线性表的初始化
//初始化
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList
//初始化一个单链表(带头节点)
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode)); //分配一个头节点
if(L == NULL){
return false;
}
L->next = NULL;
return true;
}
void test(){
LinkList L;//声明一个指向单链表的节点
InitList(L);
}
线性表的判空
//判断链表是否为空,只需要判断头节点是不是空的
bool Empty(LinkList L){
if(L == NULL){
return true;
}else{
return false;
}
}
线性表的基本操作
插入操作
按位序插入(带头节点)
bool ListInsert(LinkList &L, int i, ElemType e){//对于带头结点的链表,情况略有不同。由于头结点不存储实际的数据,
//它主要用于简化某些操作(如插入和删除),并且其next指针指向链表的第一个实际数据节点。
//因此,在插入函数中,我们通常只需要传递头结点的指针(而不是头指针的指针),因为头结点本身不会改变。
if(i<1){
return false;
}
LinkList p = L;//这里要把p初始化为头节点
// LNode *p = L;//也可以
int j = 0;
while(p!=NULL && j<i-1){
p = p->next;
j++;
}
if(p == NULL){
return false;
}
LNode *t = (LNode *)malloc(sizeof(LNode));
t->data = e;
t->next = p->next;
p->next = t;
return true;
}
按位序插入(不带头节点)
//按位序插入(不带头节点)
bool LinkInsert(LinkList *L, int i, ElemType e){//对于不带头结点的链表,我们通常需要传递链表的头指针(即第一个节点的指针)给插入函数。
if(i<1||L == NULL||*L == NULL){
return false;
}
LinkList p = *L;
// LNode *p = *L;//也可以
int j = 1;
//寻找第i - 1个节点
while(p!=NULL&&j<i-1){
p = p->next;
j++;
}
if(p == NULL){
return false;
}
LNode *t = (LinkList)malloc(sizeof(Lnode));
if(t == NULL) return false;//内存分配失败
t->data = e;
t->next = p->next;
p->next = t;
if(i == 1){
*L = t;
}
return true;
}
指定节点的后插操作
//指定节点的后插操作
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList
//后插操作:在p节点后插入元素e
bool InsertNextNode(LNode *p, ElemType e){
if(p == NULL){
return false;
}
LNode *t = (LNode *)malloc(sizeof(LNode));
if(t == NULL){
return false;
}
t->data = e;
t->next = p->next;
p->next = t;
return true;
}
指定节点的前插操作
//前插操作
bool InsertNextNode(LNode *p, ElemType e){
if(p == NULL){
return false;
}
LNode *t = (LNode *)malloc(sizeof(LNode));
if(t == NULL){
return false;
}
//其实前插法也是尾插法,不过是吧t插到p的后面,然后吧t和p的值互换
t->next = p->next;
p->next = t;
t->data = p->data;
p->data = e;
}
删除操作
带头结点的删除操作
typedef struct LNode{
ElemType data;
struct *LNode *next;
}LNode, *LinkList
//带头结点的删除操作
bool ListDelete(LinkList &L, int i, ElemType &e){//删除操作的思路就是找到第i位的前一位,然后i-1的指针指向i的指针指向的节点
if(i<1){
return false;
}
LinkList p = L;
int j = 0;
while(p!=NULL&&j<i-1){//j最大到i-2,当j等于i - 2的时候p = p->next;此时p是第i-1个元素
p = p->next;
j++;
}
if(p == NULL){//i值不合法
return false;
}
if(p->next == NULL){//i-1节点后面已经没有节点了,那要进行删除操作,删除你🐎啊
return false;
}
LNode *q = p->next;//新声明一个指针q让它指向第i个元素
e = q->data;//得到第i个元素的值
p->next = q->next;//第i-1个元素的下一个节点变成第i个元素的下一个节点
free(q);//释放q的内存
return true;
}
不带头节点的删除操作
其实写到这里,如果你前面都认真看了的话,在插入操作里包括了带头节点和不带的情况,我这里做一个简单的整理,带与不带,其实差不多,带头节点的话,传入的是指向头节点的指针或者是指向头节点指针的指针,而不带的话,就是第一个节点,第一个节点和头节点的唯一区别就是头节点(哨兵)不带数据域(data)。那么搞清楚这点,就很明了了。
查找操作
查找(带头节点)
//查找操作(带头节点)
LNode *getElem(LinkList L, int i){//这里传入的是指向链表头节点的指针
if(i<0){
return NULL;//这里为什么返回NULL,因为这个函数的返回值是LNode *类型的
}
LNode *p;
//LinkList p;//也可以
int j = 0;
p = L;
while(p!=NULL&&j<i){
p = p->next;
j++;
}
return p;
}
按值查找
//按值查找操作
LNode *LocateElem(LinkList L, ElemType e){
if(L == NULL){
return NULL;//链表为空就返回NULL
}
LNode *p = L;//哨兵节点不带data
while(p->next!=NULL&&P->data!=e){
p = p->next;
}
return (p!=NULL&&p->data == e) ? p : NULL;
}
创建链表
尾插法
//尾插法
LinkList List_TailInsert(LinkList &L){//尾插法其实就是搞一个表尾指针,始终指向的是表尾节点,然后每次在表尾插入节点s,然后把s和r互换
int x;
L = (LinkList)malloc(sizeof(LNode));
LNode *s, *r = L;//前者是表头指针,后者是表尾指针,开始的时候都是指着哨兵节点
scanf("%d", &x);
while(x!=9999){
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;//交换
scanf("%d", &x);
}
r->next = NULL;
return L;
}
头插法
//头插法
LinkList List_HeadInsert(LinkList &L){//头插法实际上是在哨兵节点后面插
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode));
L->next = NULL;
scanf("%d", &x);
while(x!=9999){
s = (LNode *)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
return L;
}
静态链表介绍
静态链表
静态链表是用数组来表示单链表,用数组元素的下标来模拟单链表的指针。这种描述方法称为游标(cursor)实现法。在静态链表中,各个结点集中安置,逻辑结构上相邻的数据元素,存储在指定的一块内存空间中,数据元素只允许在这块内存空间中随机存放。实际上,静态链表就是一个结构体数组。
相较于一般的链表,静态链表有以下优点:
不需要频繁地进行内存分配和释放,因为静态链表的空间是预先分配好的,可以直接使用。
相对于数组,静态链表可以更方便地进行插入和删除操作,因为只需要修改指针,不需要移动元素。
静态链表可以避免内存碎片的问题,因为所有元素都是连续存储的。
然而,静态链表也有其局限性:
静态链表的大小是固定的,不能动态地扩展或缩小,因此可能会浪费一些空间。
静态链表的插入和删除操作需要修改指针,可能会导致指针混乱或者出现空洞,需要额外的维护工作。
静态链表的访问速度可能会比较慢,因为需要通过指针来访问元素,而指针的访问速度相对较慢。
此外,静态链表主要是为了给没有指针的高级语言设计的一种实现单链表能力的方法,其思想非常巧妙,但不一定在所有情况下都适用。
循环链表
循环链表(circular linked list)是另外一种形式的链式存储结构,它的特点是从表中最后一个结点的指针域指向头结点,整个链表形成一个环,由此,从表中任一结点触发均可找到表中的其他结点
循环链表的操作和线性表基本一致,差别仅在于算法中的循环条件不是P—>next是否为空,而是是否等于头指针
双向链表
双向链表的操作
删除&插入
//插入操作&删除操作
typedef struct DNode{
ElemType data;
struct DNode *prior, *next;
}DNode, *DLinkList;
//在p节点后插入s节点
bool InsertNextDNode(DNode *p, DNode *s){
if(p == NULL || s == NULL){
return false;
}
s->next = p->next;
if(p->next != NULL){
p->next->prior = s;
}
//很好理解的
s->prior = p;
p->next = s;
return true;
}
// 删除p结点的后继结点
bool DeletenextDNode(DNode *p){
if(p == NULL || s == NULL){ //非法参数
return false;
}
DNode *p = p->next; //找到p的后继结点q
if(q == NULL){
return false; //p没有后继
}
if(q->next != NULL){ //q结点不是最后一个结点
q->next->prior = p;
}
free(q); //释放结点空间
return true;
}
Written by hcx
--2024/4/23