哈喽!这里是一只派大鑫,不是派大星。本着基础不牢,地动山摇的学习态度,从基础的C语言语法讲到算法再到更高级的语法及框架的学习。更好地让同样热爱编程(或是应付期末考试 狗头.jpg)的大家能够在学习阶段找到好的方法、路线,让天下没有难学的程序(只有秃头的程序员 2333),学会程序和算法,走遍天下都不怕!

目录

引言

一、什么是线性表

二、常见的线性表

前驱和后继

三、顺序表

3.1顺序存储的定义

3.2顺序表的存储结构

3.3顺序表的常用操作

3.3.1初始化

3.3.2添加

3.3.3删除

3.3.4修改

3.3.5查找

3.3.6遍历

3.4顺序表完整代码

3.5顺序表的优缺点:

四、单链表

4.1链式存储的定义

4.2单链表的存储结构

4.3单链表的常用操作

4.3.1初始化

4.3.2添加

4.3.3删除

4.3.4修改

4.4.5查找

4.4.6遍历

4.4单链表完整代码

4.5单链表的优缺点

五、循环链表

5.1循环链表的存储结构

5.2循环链表的常用操作

5.2.1初始化

5.2.2判断空

5.2.3插入

5.2.4删除

5.2.5遍历

5.3循环链表完整代码

六、双向链表

6.1双向链表的存储结构

6.2双向链表的常用操作

6.2.1初始化

6.2.2判断空

6.2.3头插法

6.2.4插入

6.2.5删除

6.2.6遍历

6.2.7双向链表特点

6.3双向链表完整代码

七、双向循环链表

7.1双向循环链表的存储结构

7.2双向链表的常用操作

7.2.1初始化

判断空

7.2.2插入

7.2.3删除

7.2.4遍历

7.3双向链表完整代码

总结


引言

本文从最基础的什么是线性表开始,依次讲解常见线性表,并给出参考代码,相信哪怕是小白的你,在看完文章后也能豁然开朗。

妈妈再也不担心我不会手写数据结构了

一、什么是线性表

线性表(List):零个或多个数据元素的有限序列。

百度百科中是这样定义线性表的:

线性表是最基本、最简单、也是最常用的一种数据结构。线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列

线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储,但是把最后一个数据元素的尾指针指向了首位结点)。

简单来说,线性表就像是一条线(或是一“串”)的结构

这就是最简单最形象的“线”,那么如果将一些数据按照类似这样的结构“连接”起来,就成了“线性表”。

二、常见的线性表

线性表的花样有很多,常见的主要是顺序表、单链表、循环链表等......

虽然这些名词看着比较高大上,但其实并不难!多看、多思、多敲,没有学不会的技术~~~

下面我将一一讲解常见的线性表结构。

前驱和后继

数据结构中,一组数据中的每个个体被称为“数据元素”(简称“元素”)

另外,对于具有“一对一”逻辑关系的数据,我们一直在用“某一元素的左侧(前边)或右侧(后边)”这样不专业的词,其实线性表中有更准确的术语:

  • 某一元素的左侧相邻元素称为“直接前驱”,位于此元素左侧的所有元素都统称为“前驱元素”;
  • 某一元素的右侧相邻元素称为“直接后继”,位于此元素右侧的所有元素都统称为“后继元素”;

三、顺序表

首当其冲的就是顺序表了,这是线性表最简单最直接的一种实现方式。

将具有“一对一”关系的数据“线性”地存储到物理空间中,这种存储结构就称为线性存储结构(简称线性表)。

不仅如此,顺序表对数据的物理存储结构也有要求。顺序表存储数据时,会提前申请一整块足够大小的物理空间,然后将数据依次存储起来,存储时做到数据元素之间不留一丝缝隙。

例如,使用顺序表存储集合 {1,2,3,4,5},数据最终的存储状态:

3.1顺序存储的定义

线性表的顺序存储结构,指的是用一段地址连续的存储单元一次存储线性表的数据元素。

由此我们可以得出,将“具有 '一对一' 逻辑关系的数据按照次序连续存储到一整块物理空间上”的存储结构就是顺序存储结构。

通过观察图中数据的存储状态,我们可以发现,顺序表存储数据同数组非常接近。

其实,顺序表存储数据使用的就是数组

3.2顺序表的存储结构

使用顺序表存储数据之前,除了要申请足够大小的物理空间之外,为了方便后期使用表中的数据,顺序表还需要实时记录以下 2 项数据:

  1. 顺序表申请的存储容量;
  2. 顺序表的长度,也就是表中存储数据元素的个数;

提示:正常状态下,顺序表申请的存储容量要大于顺序表的长度。

#define MAXSIZE 20  //存储空间初始化大小为20
typedef struct SqList{int data[MAXSIZE]; //利用数组来存储数据元素int length;  //记录线性表的长度
}SqList;

于是乎,我们的顺序表存储结构就是这样得到了~~~

3.3顺序表的常用操作

光有存储结构也只是顺序表的一小部分,就像人不能光有脑袋,而没有身体来进行相应的“动作”。

3.3.1初始化

其实在定义结构体时,我们就引入了数组,所以无需再进行指针啊什么乱七八糟的初始化操作了。

void InitList(SqList *l){l->length = 2;
}

诶?那还没有指明具体的顺序表是哪一个呢? 没错,我们当然要建立一个顺序表才能使用!

int main(){SqList list;//定义一个顺序表InitList(&list);
}

3.3.2添加

想象我们在生活中排队时,如果中间有人插队的话,那么在他插入的那个位置之后的人,都得需要往后站一站(给他腾出位置)这样才能“插队成功”。

(插队可耻,知道插队会影响多少人了吧!)

顺序表即是如此,如果我们在指定位置插入元素,那么我们需要先将其位置以及之后位置的元素往挪一位,再在此位置添加元素。

//添加
void Insert(SqList *L,int i,int e){ int k;if (L->length == MAXSIZE-1){  /* 顺序线性表已经满 */printf("容量已满,插入失败\n");return;}if (i<0 || i>=L->length+1){/* 当i比第一位置小或者比最后一位置后一位置还要大时 */printf("插入位置有误\n");return;}if (i<L->length)        /* 若插入数据位置不在表尾 */{for(k=L->length;k>i;k--)  /* 将要插入位置之后的数据元素向后移动一位 */L->data[k]=L->data[k-1];}L->data[i]=e;          /* 将新元素插入 */L->length++;printf("添加成功\n");}

3.3.3删除

想象我们在生活中排队时,如果中间有人离队的话,那么在他插入的那个位置之后的人,都得可以往前站一站,不能留个空位置出来给别人插队的机会是吧。

顺序表即是如此,如果我们在指定位置删除元素,那么我们需要先将其位置之后的元素往挪一位。

//删除
void Del(SqList *L,int i){int k;if (L->length==0){               /* 线性表为空 */printf("当前顺序表为空,不能再删除了!\n");return;}if (i<0 || i>=L->length){         /* 删除位置不正确 */printf("哦豁,指定位置有误,无法删除\n");return;}if (i<L->length-1)                /* 如果删除不是最后位置 */{for(k=i;k<L->length-1;k++)/* 将删除位置后继元素前移 */L->data[k]=L->data[k+1];}L->length--;printf("删除成功\n");
}

3.3.4修改

修改也很简单,还是老师点名:8号同学,你叫李狗蛋是吗?

8号同学听到立马不乐意了:老师,我叫李全蛋!

老师可吓了一跳,立马在8号这个位置把李狗蛋划掉,重新写上李全蛋。

于是老师以后就不会念错名字了~~~

//修改
void Change(SqList *l,int index,int value){//将下标index的值修改为valueif(index < 0 || index >= l->length){printf("查无此人,修改失败\n");return;}l->data[index] = value;printf("已修改下标%d的值为%d\n",index,value);
}

3.3.5查找

顺序表 顾名思义,就是按着规定的顺序来的,所以如果我们需要查找某个指定位置上的元素,那么直接就通过数组下标查询即可。

通俗来讲,老师想要点名,但是呢又不认识同学们(假设是新老师刚开学),他找班主任要了一份名单,名单上有学号和对应的同学,那么他只需要大声喊:8号同学,你起来自我介绍一下。 如此一来,老师就可以知道学号为8的同学叫什么名字了。

顺序表就有这样的好处,直接根据指定位置得到结果~~~

//查找
int Visit(SqList l,int index){//访问下标为index的元素的值return l.data[index];
}

3.3.6遍历

遍历就相当于老师拿着学生名单从头到尾念一遍学号,同学们答:到!

为了保证不漏掉任何一个可能逃课的同学,老师当然要从学号为1的开始点名。

(在数组中我们起始下标从0开始)

//遍历
void Print(SqList l){if(l.length == 0){printf("表空\n");return;}printf("====打印顺序链表====\n");for(int i = 0; i < l.length; i++){printf("%d ",l.data[i]);}printf("\n");
}

3.4顺序表完整代码

#include<stdio.h>
#include<iostream>
#include<math.h>
using namespace std;#define MAXSIZE 20  //存储空间初始化大小为20
typedef struct SqList{int data[MAXSIZE]; //利用数组来存储数据元素int length;  //记录线性表的长度
}SqList;//初始化
void InitList(SqList *l){l->length = 0;
}//添加
void Insert(SqList *L,int i,int e){ int k;if (L->length == MAXSIZE-1){  /* 顺序线性表已经满 */printf("容量已满,插入失败\n");return;}if (i<0 || i>=L->length+1){/* 当i比第一位置小或者比最后一位置后一位置还要大时 */printf("插入位置有误\n");return;}if (i<L->length)        /* 若插入数据位置不在表尾 */{for(k=L->length;k>i;k--)  /* 将要插入位置之后的数据元素向后移动一位 */L->data[k]=L->data[k-1];}L->data[i]=e;          /* 将新元素插入 */L->length++;printf("添加成功\n");}//删除
void Del(SqList *L,int i){int k;if (L->length==0){               /* 线性表为空 */printf("当前顺序表为空,不能再删除了!\n");return;}if (i<0 || i>=L->length){         /* 删除位置不正确 */printf("哦豁,指定位置有误,无法删除\n");return;}if (i<L->length-1)                /* 如果删除不是最后位置 */{for(k=i;k<L->length-1;k++)/* 将删除位置后继元素前移 */L->data[k]=L->data[k+1];}L->length--;printf("删除成功\n");
}//修改
void Change(SqList *l,int index,int value){//将下标index的值修改为valueif(index < 0 || index >= l->length){printf("查无此人,修改失败\n");return;}l->data[index] = value;printf("已修改下标%d的值为%d\n",index,value);
}//查找
int Visit(SqList l,int index){//访问下标为index的元素的值return l.data[index];
}//遍历
void Print(SqList l){if(l.length == 0){printf("表空\n");return;}printf("====打印顺序链表====\n");for(int i = 0; i < l.length; i++){printf("%d ",l.data[i]);}printf("\n");
}int main(){SqList list;//定义一个顺序表InitList(&list);//printf("%d",list.length); //因为不是指针,所以直接用 . 来调用属性Insert(&list,0,11);Insert(&list,1,12);Insert(&list,2,13);Insert(&list,3,14);Insert(&list,4,15);Del(&list,2);Change(&list,2,10);printf("查找下标%d的结果为:%d\n",3,Visit(list,3));Print(list);printf("顺序表的长度为:%d\n",list.length);return 0;
}

3.5顺序表的优缺点:

特点 : 基于一维数组的实现,使用顺序存储结构,即存储时使用的连续的存储空间

优点:查询效率高,因为采用了顺序存储结构,所有元素的空间是连续的。

缺点:容量容易达到上限,在进行插入和删除时需要进行大量的数据移动;要求存储空间必须是连续的。

四、单链表

4.1链式存储的定义

链表是物理存储单元上非连续、非顺序的存储结构。与我们之前学习过的数组同为存储结构,区别是数组是连续的、顺序的存储结构。

在链表这种非连续、非顺序的存储结构中,每个元素以结点的形式存储。而每个结点都由数据域和指针域构成。

关于头结点

  链表可以有头结点也可以没有,区别在于链表有头结点虽浪费空间,但易理解,边界好处理,不易出错,代码简单,相反无头结头节省空间,难理解,边界不易处理,代码稍复杂。有头结点的引入是为了对链表删除、逆向、建立的时候操作更统一,不用专门对第一个元素或最后一个元素进行单独处理。

4.2单链表的存储结构

1.链表是结构、指针相结合的一种应用,它是由头、中间、尾多个链环组成的单方向可伸缩的链表,链表上的链环我们称之为结点。

2.每个结点的数据可用一个结构体表示,该结构体由两部分成员组成:数据成员与结构指针变量成员。

3.数据成员存放用户所需数据,而结构指针变量成员则用来连接(指向)下一个结点,由于每个结构指针变量成员都指向相同的结构体,所以该指针变量称为结构指针变量。

4.链表的长度是动态的,当需要建立一个结点,就向系统申请动态分配一个存储空间,如此不断地有新结点产生,直到结构指针变量指向为空(NULL)。申请动态分配-个存储空间的表示形式为:           (struct  Node*)malloc(sizeof(struct  Node))

//单链表的存储结构
typedef struct Node{int data;struct Node *next;
}Node,*LinkList;

此处定义单链表每个结点的存储结构,包括 存储结点的数据域 data 和 存储后继结点位置的指针域 next 。

对同一结构体指针类型命了两个名 LinkListNode * ,两者本质上是等价的。

通常习惯用 LinkList 定义单链表,表示某单链表的头指针;用LNode *定义指向单链表中任意结点的指针变量。

如: 定义LinkList L ,则L为单链表的头指针;   定义Node *p ,则p为指向单链表中某个结点的指针,用 *p 表示该结点。

单链表由表头指针唯一确定,因此单链表可以用头指针的名字来命名,如头指针名为 L ,则简称该链表为为表 L。

注意区分指针变量和结点变量两个不同概念,若定义 LinkList p 或 LNode *p ,则p为指向某结点的指针变量,表示该结点的地址;而*p为对应结点的变量,表示该结点的名称。

此处为了 便于首元结点的处理、便于空表和非空表的统一处理为链表增加头结点。

4.3单链表的常用操作

4.3.1初始化

1.生成一个新结点作为头结点 ,用头指针 L 指向头结点。

2.头结点的指针域置空。

可以使用关键字 new 为结点分配内存,也可以用头文件 stdlib.h 中的 malloc 分配内存。

//初始化
LinkList InitList(){LinkList L = (Node*)malloc(sizeof(Node));L->next = NULL;return L;
}

初始化操作是创建一个只有头结点的空链表,那么如何创建包括若干结点的链表呢?

  创建链表时根据结点插入位置不同,链表的创建方法可分为前插法和后插法

  • 前插法

前插法通过将新结点逐个插入链表的头部(头结点之后)来创建链表,每次申请一个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后 。

    1.创建一个只有头结点的空链表。

    2.根据创建链表结点元素个数 n ,循环n次执行以下操作:

      (1)生成一个新结点 *p ;

      (2)将输入元素赋新结点 *p 的数据域中;

      (3)将新结点 *p 插入到头结点之后。

//前插法
void Create_H(LinkList L,int n){Node *p;printf("输入%d个元素:\n",n);for(int i = 0; i < n; i++){p = (Node *)malloc(sizeof(Node));scanf("%d",&(p->data));p->next = L->next;L->next = p;}
}
  • 后插法

  后插法通过将新结点逐个插入到链表的尾部来创建链表。同前插法,每次申请一个新结点,读入相应的数据元素值。不同的是,为了使新结点能够插入到单链表尾部,需要增加一个尾指针 rear 指向链表的尾结点。

    1.创建一个只有头结点的空链表。

    2.尾指针 rear 初始化,指向头结点。

    3.执行以下操作:

      (1)生成一个新结点  *p ;

      (2)输入元素值赋给新结点 *p 的数据域;

      (3)将新结点 *p 插入到 *r 之后;

      (4)尾指针tailNode指向新的尾结点 *p。

//后插法
void Create_R(LinkList L,int n){Node *p,*rear = L;printf("输入%d个元素:\n",n);for(int i = 0; i < n; i++){p =(Node *)malloc(sizeof(Node));scanf("%d",&(p->data));p->next = NULL;rear->next = p;rear = p;}
}

4.3.2添加

创建了含若干个结点的单链表,就可能根据需求对链表进行新结点的插入操作。

  将数据域为 e 的新结点插入到单链表的第 i 个结点位置上,即插入到结点  Ni-1 和 Ni 之间。

  1.查找结点 Ni-1,并由指针 p 指向该结点;

  2.生成一个新的结点  *s ;

  3.将新结点 *s 数据域置为 e,指针域指向结点 Ni;

  4.将结点 *p 的指针域指向新结点 *s。

//添加
void Insert(LinkList L,int index,int e){Node *p = L,*s = (Node *)malloc(sizeof(Node));s->data = e; s->next = NULL;int i = 0;while(p && i < index-1){//因为我们要插入到index的位置,所以找到其前一个指针位置(index-1)p = p->next;i++;}s->next = p->next;p->next = s;
}

4.3.3删除

单链表对其中某个结点的删除也是基本操作,同单链表插入元素一样,将某个结点上从链表中删除,首先查找结点 Ni-1然后删除该位置结点。

  1.查找结点 Ni-1 位置,并由指针 p 指向该结点;

  2.临时保存待删除结点 Ni 地址在 q 中,以备释放结点空间;

  3.将结点 *p 指针域指向结点 Ni 的的后继结点;

  4.释放结点 Ni 的空间。

简单来说就是找到其前一个位置,然后它的前一个直接指向它的后一个位置。

//删除
void Delete(LinkList L,int index){Node *p = L,*q;int i = 0;while(p->next && i < index-1){p = p->next;i++;}if(!(p->next)){printf("删除失败,下标有误\n");return;}q = p->next;p->next = q->next;free(q);
}

4.3.4修改

像遍历一样,将一个辅助指针(p指针)移动到index的位置上,直接对其data的值进行修改即可

//修改
void Change(LinkList L,int index,int newValue){Node *p = L;int i = 0;while(p->next && i < index){p = p->next;i++;}if(p)p->data = newValue;
}

4.4.5查找

查找data为指定值的index下标(头结点下标为0),依次进行遍历即可

//查找
int GetIndex(LinkList L,int value){Node *p = L;int i = 0;while(p && p->data != value){p = p->next;i++;}if(p) return i;else return 0;
}

4.4.6遍历

由于我们引入了头结点,所以第一个结点应该是头结点指向的下一个结点,

因此我们从p->next开始输出,直到p为NULL就退出循环即可。

void Print(LinkList L){printf("开始遍历单链表:\n")Node *p = L->next;while(p != NULL){printf("%d ",p->data);p = p->next;}
}

4.4单链表完整代码

#include<stdio.h>
#include<iostream>
#include<math.h>
using namespace std;
//单链表的存储结构
typedef struct Node{int data;struct Node *next;
}Node,*LinkList;
//初始化
LinkList InitList(){LinkList L = (Node*)malloc(sizeof(Node));L->next = NULL;return L;
}
//前插法
void Create_H(LinkList L,int n){Node *p;printf("输入%d个元素:\n",n);for(int i = 0; i < n; i++){p = (Node *)malloc(sizeof(Node));scanf("%d",&(p->data));p->next = L->next;L->next = p;}
}
//后插法
void Create_R(LinkList L,int n){Node *p,*rear = L;printf("输入%d个元素:\n",n);for(int i = 0; i < n; i++){p =(Node *)malloc(sizeof(Node));scanf("%d",&(p->data));p->next = NULL;rear->next = p;rear = p;}
}
//添加
void Insert(LinkList L,int index,int e){Node *p = L,*s = (Node *)malloc(sizeof(Node));s->data = e; s->next = NULL;int i = 0;while(p && i < index-1){//因为我们要插入到index的位置,所以找到其前一个指针位置(index-1)p = p->next;i++;}s->next = p->next;p->next = s;
}
//删除
void Delete(LinkList L,int index){Node *p = L,*q;int i = 0;while(p->next && i < index-1){p = p->next;i++;}if(!(p->next)){printf("删除失败,下标有误\n");return;}q = p->next;p->next = q->next;free(q);
}
//修改
void Change(LinkList L,int index,int newValue){Node *p = L;int i = 0;while(p->next && i < index){p = p->next;i++;}if(p)p->data = newValue;
}
//查找
int GetIndex(LinkList L,int value){Node *p = L;int i = 0;while(p && p->data != value){p = p->next;i++;}if(p) return i;else return 0;
}
//遍历
void Print(LinkList L){printf("开始遍历单链表:\n");Node *p = L->next;while(p != NULL){printf("%d ",p->data);p = p->next;}printf("\n");
}
int main(){LinkList L = InitList();Create_H(L,4);//Create_R(L,4);Insert(L,2,99);Print(L);//Delete(L,2);Change(L,2,88);Print(L);printf("查找下标为:%d\n",GetIndex(L,6));return 0;
}

4.5单链表的优缺点

优点:元素的存储单元是任意的,可连续也可不连续。

不需要限定长度。

添加元素和删除元素比较方便快捷。

缺点:查找时间复杂度为O(n)。

存放元素时需要另外开辟一个指针域的空间。

熟悉了顺序表和单链表后,对于循环链表、双向链表、双向循环链表学习起来就很轻松了

五、循环链表

5.1循环链表的存储结构

循环列表是一种特殊的单链表,它跟单链表唯一的区别就在于它的尾结点又指回了链表的头结点首尾相连,形成了一个环,所以叫做循环链表。

与单链表相比,循环链表的优点是从链尾到链首比较方便,适用于处理具有环形结构的数据问题,比如著名的约瑟夫问题。

循环链表的存储结构和单链表一样~~~

typedef struct Node{int data;struct Node *next;
}Node,*CLinkList;

5.2循环链表的常用操作

5.2.1初始化

初始化与单链表稍有不同,这里我们将头结点的next指向它自己,形成了一个环。

//初始化
CLinkList InitList(){CLinkList L = (CLinkList)malloc(sizeof(Node));L->next = L;return L;
}

这里我们就采用头插法来为循环链表进行初始化

:这里无论L是否为空,都不影响结点插入的方式,可以自己思考思考为什么

//头插法
void Create_H(CLinkList L,int n){Node *p;printf("头插法,输入%d个数:\n",n);for(int i = 0; i < n; i++){p = (Node *)malloc(sizeof(Node));scanf("%d",&(p->data));p->next = L->next;L->next = p;}
}

5.2.2判断空

循环链表判断是否为空是非常容易的,如果L的next指向L,则代表链表空,返回1

//判断空
int Judge(CLinkList L){if(L->next == L)return 1;//为空 返回1return 0;
}

5.2.3插入

插入操作和单链表差别不大,如果链表为空,我们将其加入到头结点之后,

如果元素e存在,则将其加入到后面,

如果元素e不存在,则插入失败。

//插入
void Insert(CLinkList L,int e,int value){//在指定元素e的后面插入valueNode *p = (Node *)malloc(sizeof(Node));p->data = value;if(Judge(L)){ //链表为空,直接添加到头结点后p->next = L->next;L->next = p;return;}Node *q = L->next;while(q != L && q->data != e)q = q->next;if(q != L){ //代表找到了元素e 这里也等价为 q->data == ep->next = q->next;q->next = p;}else{printf("找不到指定元素%d,插入失败\n",e);}
}

5.2.4删除

删除操作和单链表差别不大,利用p的next的值作为循环终止条件的话,就不用再加一个辅助指针前后一起移动了

//删除
void Delete(CLinkList L,int e){ //删除指定元素eNode *p = L->next;while(p != L && p->next->data != e)p = p->next;if(p == L){printf("找不到%d,删除失败\n",e);}else{Node *q = p->next;p->next = q->next;free(q);}
}

5.2.5遍历

从L开始依次遍历即可,需要注意的是因为链表是循环的,我们总不能一直遍历下去吧,也不能重复遍历了,遍历一次的结束循环的条件就是p == L

//遍历
void Print(CLinkList L){if(Judge(L)) return;printf("循环链表遍历:\n");Node *p = L->next;while(p != L){printf("%d ",p->data);p = p->next;}printf("\n");
}

5.3循环链表完整代码

#include<stdio.h>
#include<iostream>
#include<math.h>
using namespace std;
typedef struct Node{int data;struct Node *next;
}Node,*CLinkList;
//初始化
CLinkList InitList(){CLinkList L = (CLinkList)malloc(sizeof(Node));L->next = L;return L;
}
//判断空
int Judge(CLinkList L){if(L->next == L)return 1;//为空 返回1return 0;
}
//头插法
void Create_H(CLinkList L,int n){Node *p;printf("头插法,输入%d个数:\n",n);for(int i = 0; i < n; i++){p = (Node *)malloc(sizeof(Node));scanf("%d",&(p->data));p->next = L->next;L->next = p;}
}
//遍历
void Print(CLinkList L){if(Judge(L)) return;printf("循环链表遍历:\n");Node *p = L->next;while(p != L){printf("%d ",p->data);p = p->next;}printf("\n");
}
//插入
void Insert(CLinkList L,int e,int value){//在指定元素e的后面插入valueNode *p = (Node *)malloc(sizeof(Node));p->data = value;if(Judge(L)){ //链表为空,直接添加到头结点后p->next = L->next;L->next = p;return;}Node *q = L->next;while(q != L && q->data != e)q = q->next;if(q != L){ //代表找到了元素e 这里也等价为 q->data == ep->next = q->next;q->next = p;}else{printf("找不到指定元素%d,插入失败\n",e);}
}
//删除
void Delete(CLinkList L,int e){ //删除指定元素eNode *p = L->next;while(p != L && p->next->data != e)p = p->next;if(p == L){printf("找不到%d,删除失败\n",e);}else{Node *q = p->next;p->next = q->next;free(q);}
}
int main(){CLinkList L = InitList();Create_H(L,4);Print(L);//Insert(L,3,99);Delete(L,3);Print(L);return 0;
}

六、双向链表

6.1双向链表的存储结构

双向链表中的每个结点具有两个方向指针,后继指针(next)指向后面的结点,前驱指针(prev)指向前面的结点。
双向链表也有两个特殊结点,首节点的前驱指针和尾结点的后继指针均指向空地址NULL

与单链表相比,储存同样的数据,双向链表会占用更多的内存空间。虽然多占用了空间,但是双向链表在处理根据已知结点查找上一节点、有序链表查找等问题上,都表现的更灵活高效。

双向链表中的结点有两个指针域,一个指向直接前趋,一个指向直接后继。(链表中第一个结点的前趋结点为NULL,最后一个结点的后继结点为NULL)

//双向链表的存储结构
typedef struct Node{int data;struct Node *next,*front;
}Node,*DLinkList;

6.2双向链表的常用操作

6.2.1初始化

双向链表初始化和单向链表的初始化差不多,只是前指针也为NULL

//双向链表初始化
DLinkList InitList(){DLinkList L = (DLinkList)malloc(sizeof(Node));L->next = NULL;L->front = NULL;return L;
}

6.2.2判断空

很简单,只需要看头结点之后是否有数据存在即可。

//判断空
int IsEmpty(DLinkList L){if(L->next == NULL) return 1; //链表为空返回1return 0;
}

6.2.3头插法

同样的,我们利用头插法来初始化数据。

在此之前我们通过一个简单的故事(案例)来体会双向链表的基本操作过程,以下的操作全都基于这个故事哦,请好好体会~~~

老师让同学们手拉手站成一排(我们假设从左到右为一排站着,左边第一个是头结点,右边最后一名是尾巴,左手就代表前指针front,右手就代表后指针next),那么除了第一个同学(代表头结点)和最后一个同学(代表最后一个结点)只用一只手来“牵着”别的同学,其余中间的同学都得用左手和右手把相邻同学的手“牵起来”。(注意,是手牵手 而不是一只手去拉扯衣服)

所有在队伍的同学只占成了一排,中间没有同学把小手放下,这就是连续性

接下来老师说:以我为基准(排头\头结点),站在我的右边,我们手拉手站成一排!一位一位的来,每次只从我的“右手”牵手,别去找别的同学啦~~~

于是头插法就有了这样的情景:

有位同学自告奋勇第一个来尝试,他一看,咦?我是第一个人,只有老师站在那

因此情况就变得简单了起来

同学:老师我来了

老师:(好小子,就你一个人,不能让你给跑了)一手把同学给牵住 L->next = p

同学一看,也立马把手交给老师 p->front = L

同学:老师,好像没有别人了欸,怎么办

老师:你就不用管了吧 p->next = NULL

这样一来我们第一个同学就完成了于老师的牵手

其他同学一看,好有趣的样子,于是也打算加入队伍

同学询问老师:老师我可以加入队伍吗

老师:可以的,既然你新来的就在我右边站着吧,其他同学已经站好了,你要保证我们的“连续性”哦

同学开始沉思,如何保证“连续性”,顺利加入队伍呢?  ten minutes later

同学对老师右边的同学说,同学:我想牵着你(先询问p->next = L->next

他又转头向老师说:老师我想牵着你 p->front = L,我已经把你旁边的同学牵上了

老师一看,确实如此,那么他就有机会加入队伍了(后同意) L->next = p

老师之前旁边的同学也知道了,既然老师都同意了,那自己也同意吧,p->next->front = p

有了两位同学的加入,其他同学也明白了,只需要像第二位同学学习 先询问后加入 的方式就能顺利入伍~~~

于是我们可以得到以下的,头插法初始化结点代码:

//头插法
void Create_H(DLinkList L,int n){Node *p;printf("头插法,输入%d个数:\n",n);for(int i = 0; i < n; i++){p = (Node*)malloc(sizeof(Node));scanf("%d",&(p->data));if(IsEmpty(L)){ //只有“老师”在,此时是“自告奋勇”第一位同学L->next = p;p->front = L;p->next = NULL;}else{ //已经有其他“同学”了p->next = L->next;p->front = L;L->next = p;p->next->front = p;}}
}

6.2.4插入

双向链表的删除类似于单链表,只不过需要改变的有前、后两个指针。

这时,其他班的同学看到了,他觉得这个“游戏”很有趣,于是找到你的老师并问他:老师我能加入你们队伍吗?

老师回答:当然可以,你找个“认识”的小伙伴(指定index插入),站在他旁边吧,但是你不能让队伍中断了,必须保证连续性!

同学早在一旁看出了“端倪”,知道怎么加入队伍才能不破坏连续性,只不过现在由小伙伴充当老师的角色~~~ 对的 还是遵循 “先询问 后加入”的规则

//插入
void Insert(DLinkList L,int index,int value){//插入到第index个结点的位置,头结点为第0个Node *p = L,*n = (Node*)malloc(sizeof(Node));n->data = value;int i = 0;while(p != NULL && i < index-1 ){ //当然找到第index-1个位置 才是他的小伙伴p = p->next;i++;}if(p){ //插入到p结点之后n->next = p->next;n->front = p;p->next = n;n->next->front = n;}
}

除中间的最复杂以外,还得考虑头和尾的特殊情况,读者自行思考一下...

6.2.5删除

如果中间的同学有点急事(人有三急嘛,总得体谅一下)想要退出队伍,那么他就需要“断开”与他左右相邻同学的手。

假设一队是这样的:甲 —乙—丙 站成一排,乙想要离队,但是又得保证队伍的连续性,

于是乙对甲说:我要走啦

甲知道了:那我牵丙去了 p->front->next = p->next

然后甲又对丙说:乙走了,你牵住我吧!

于是甲和丙的手牵起来了,保证了队伍的连续性 p->next->front = p->front

如下图所示,清楚地体现了删除中间结点的操作过程,可以得到以下代码:

//删除
void Delete(DLinkList L,int index){//删除第index个结点,头结点为第0个if(IsEmpty(L)){printf("链表为空\n");return;}Node *p = L;int i = 0;while(p != NULL && i < index){p = p->next;i++;}if(p){p->front->next = p->next;p->next->front = p->front;free(p);}
}

中间的最复杂以外,还得考虑头和尾的特殊情况,读者自行思考一下...

6.2.6遍历

和单链表一样,从头结点开始,直到指针为NULL结束,依次对指针取next的值来遍历

//遍历
void Print(DLinkList L){if(IsEmpty(L)){printf("链表为空\n");return;}Node *p = L->next;while(p != NULL){printf("%d ",p->data);p = p->next;}printf("\n");
}

6.2.7双向链表特点

  • 每次在插入或删除某个节点时, 需要处理四个节点的引用, 而不是两个. 也就是实现起来要困难一些
  • 并且相当于单向链表, 必然占用内存空间更大一些.
  • 但是这些缺点和我们使用起来的方便程度相比, 是微不足道的.

6.3双向链表完整代码

#include<stdio.h>
#include<iostream>
#include<math.h>
using namespace std;
//双向链表的存储结构
typedef struct Node{int data;struct Node *next,*front;
}Node,*DLinkList;
//双向链表初始化
DLinkList InitList(){DLinkList L = (DLinkList)malloc(sizeof(Node));L->next = NULL;L->front = NULL;return L;
}
//判断空
int IsEmpty(DLinkList L){if(L->next == NULL) return 1; //链表为空返回1return 0;
}
//头插法
void Create_H(DLinkList L,int n){Node *p;printf("头插法,输入%d个数:\n",n);for(int i = 0; i < n; i++){p = (Node*)malloc(sizeof(Node));scanf("%d",&(p->data));if(IsEmpty(L)){L->next = p;p->front = L;p->next = NULL;}else{p->next = L->next;p->front = L;L->next = p;p->next->front = p;}}
}
//插入
void Insert(DLinkList L,int index,int value){//插入到第index个结点的位置,头结点为第0个Node *p = L,*n = (Node*)malloc(sizeof(Node));n->data = value;int i = 0;while(p != NULL && i < index-1 ){ //当然找到第index-1个位置 才是他的小伙伴p = p->next;i++;}if(p){ //插入到p结点之后n->next = p->next;n->front = p;p->next = n;n->next->front = n;}
}
//删除
void Delete(DLinkList L,int index){//删除第index个结点,头结点为第0个if(IsEmpty(L)){printf("链表为空\n");return;}Node *p = L;int i = 0;while(p != NULL && i < index){p = p->next;i++;}if(p){p->front->next = p->next;p->next->front = p->next;free(p);}
}
//遍历
void Print(DLinkList L){if(IsEmpty(L)){printf("链表为空\n");return;}Node *p = L->next;while(p != NULL){printf("%d ",p->data);p = p->next;}printf("\n");
}
int main(){DLinkList L = InitList();Create_H(L,5);Print(L);Insert(L,3,99);Print(L);Delete(L,3);Print(L);return 0;
}

七、双向循环链表

7.1双向循环链表的存储结构

双向循环链表,顾名思义,就是双向链表+循环链表的结合体~~~

如同双向链表一样,有前、后两个指针,以及一个数据域

非常清晰的就表达了“双向的结构”

因此双向循环链表的存储结构如下代码所示:

//双向循环链表存储结构
typedef struct Node{int data;struct Node *next,*front;
}Node,*DCLinkList;

7.2双向链表的常用操作

7.2.1初始化

在存储结构中我们已经完成了“双向”的定义,那么对于“循环”,当然就想循环链表一样了,

对于头结点,我们首先将其next指针指向自己,将front指针置为空(当然也可以指向L自己)

此时头结点就初始化完成了!

代码如下:

//初始化
DCLinkList InitList(){DCLinkList L = (DCLinkList)malloc(sizeof(Node));L->next = L;L->front = NULL;return L;
}

判断空

既然也是循环链表,所以判断链表是否为空的标准就是 头指针的next指针是否指向它自己

//判断空
int IsEmpty(DCLinkList L){//链表为空返回1,否则返回0if(L->next == L) return 1;return 0;
}

同样的,我们这里采用头插法来初始化链表的数据

对于第一个加入的结点和非首次加入的结点我们分别处理,

需要注意的是对第一个加入的结点,我们需要额外的一步,就是将它的尾指针指向头结点

因为我们是头插法,所以在第二个及以后结点加入链表,就不用考虑把链表“循环”起来了。

//头插法
void Create_H(DCLinkList L,int n){Node *p;printf("头插法,输入%d个数:\n",n);for(int i = 0; i < n; i++){p = (Node*)malloc(sizeof(Node));scanf("%d",&(p->data));if(IsEmpty(L)){L->next = p;p->front = L;L->front = p;p->next = L;}else{p->next = L->next;p->front = L;p->next->front = p;L->next = p;}}
}

7.2.2插入

首先我们要找到当前插入结点位置的前一个结点,找到之后也就是图中的结点p,再找到后继结点q,把p的后继结点指向新节点,新节点的前继指向p,q的前继指向新节点,新节点的后继指向q,此时便添加完成!

//插入
void Insert(DCLinkList L,int index,int value){//插入到第index个结点的位置,头结点为第0个Node *p = L,*n = (Node*)malloc(sizeof(Node));n->data = value;int i = 0;while(p != NULL && i < index-1 ){ //当然找到第index-1个位置 才是他的小伙伴p = p->next;i++;}if(p){ //把n结点 插入到 p结点之后 q之前n->front = p;n->next = p->next;p->next->front = n;p->next = n;}
}

7.2.3删除

首先找到待删除结点

然后直接将待删除结点的后继结点的front指针指向前驱结点 p->next->front = p->front;

将前驱结点的next指针指向后继结点 p->front->next = p->next;

即可

//删除
void Delete(DCLinkList L,int index){//删除第index个结点,头结点为第0个if(IsEmpty(L)){printf("链表为空\n");return;}Node *p = L->next;int i = 1;while(p != L && i < index){p = p->next;i++;}if(p != L){ //删除的下标无误p->next->front = p->front;p->front->next = p->next;free(p);}
}

7.2.4遍历

遍历只需从头结点开始,到指向头结点的结点结束即可~~

或者我们可以从头结点的下一个结点开始(也就是第一个结点,当链表非空时)遍历到头结点结束

//遍历
void print(DCLinkList L){if(IsEmpty(L)){printf("链表为空!\n");return;}Node *p = L->next;while(p !=L ){printf("%d ",p->data);p = p->next;}printf("\n");
}

7.3双向链表完整代码

#include<stdio.h>
#include<iostream>
#include<math.h>
using namespace std;
//双向循环链表存储结构
typedef struct Node{int data;struct Node *next,*front;
}Node,*DCLinkList;
//初始化
DCLinkList InitList(){DCLinkList L = (DCLinkList)malloc(sizeof(Node));L->next = L;L->front = NULL;return L;
}
//判断空
int IsEmpty(DCLinkList L){//链表为空返回1,否则返回0if(L->next == L) return 1;return 0;
}
//头插法
void Create_H(DCLinkList L,int n){Node *p;printf("头插法,输入%d个数:\n",n);for(int i = 0; i < n; i++){p = (Node*)malloc(sizeof(Node));scanf("%d",&(p->data));if(IsEmpty(L)){L->next = p;p->front = L;L->front = p;p->next = L;}else{p->next = L->next;p->front = L;p->next->front = p;L->next = p;}}
}
//插入
void Insert(DCLinkList L,int index,int value){//插入到第index个结点的位置,头结点为第0个Node *p = L,*n = (Node*)malloc(sizeof(Node));n->data = value;int i = 0;while(p != NULL && i < index-1 ){ //当然找到第index-1个位置 才是他的小伙伴p = p->next;i++;}if(p){ //把n结点 插入到 p结点之后 q之前n->front = p;n->next = p->next;p->next->front = n;p->next = n;}
}
//删除
void Delete(DCLinkList L,int index){//删除第index个结点,头结点为第0个if(IsEmpty(L)){printf("链表为空\n");return;}Node *p = L->next;int i = 1;while(p != L && i < index){p = p->next;i++;}if(p != L){ //删除的下标无误p->next->front = p->front;p->front->next = p->next;free(p);}
}
//遍历
void print(DCLinkList L){if(IsEmpty(L)){printf("链表为空!\n");return;}Node *p = L->next;while(p !=L ){printf("%d ",p->data);p = p->next;}printf("\n");
}
int main(){DCLinkList L = InitList();Create_H(L,5);Insert(L,2,99);print(L);Delete(L,2);print(L);return 0;
}

总结

自此,数据结构的链表部分就全部讲解完毕了,如果你坚持看完了文章,相信你一定收获不少。

希望能够帮助大家更好地学习数据结构,鄙人不才,文章中有错误的地方还请读者在底下留言,我会及时进行修正~~~

同时也三连+关注支持一下呀,后序内容也会逐步更新的!!!感谢老铁们的支持

《数据结构C语言版》——线性表详解,你一定能够看得懂学得会的宝典相关推荐

  1. c语言线性表库函数大全,数据结构(C语言版)-线性表习题详解

    <数据结构(C语言版)-线性表习题详解>由会员分享,可在线阅读,更多相关<数据结构(C语言版)-线性表习题详解(23页珍藏版)>请在人人文库网上搜索. 1.数 据 结 构 ,线 ...

  2. 严蔚敏数据结构C语言版——线性表的链式存储方式详细代码

    一.严蔚敏数据结构C语言版 由于书上的许多地方都是伪代码,所以下面的代码对课本上的做了一些改动,使代码能够正常运行 链表的定义即相关类型定义 typedef int ElementType; type ...

  3. 《数据结构C语言版》——二叉树详解(图文并茂)

    哈喽!这里是一只派大鑫,不是派大星.本着基础不牢,地动山摇的学习态度,从基础的C语言语法讲到算法再到更高级的语法及框架的学习.更好地让同样热爱编程(或是应付期末考试 狗头.jpg)的大家能够在学习阶段 ...

  4. 数据结构C++语言版 -- 线性表

    大二学生的 C++数据结构 有部分Openjudge提交的代码没有删除 邮箱:liu_772021@yeah.net 欢迎交流讨论~ 有用的话,点赞留言就可以表示感谢啦 #include <st ...

  5. 判断数组中某个元素除自身外是否和其他数据不同_算法工程师要懂的3种算法数据结构:线性表详解...

    算法思想有很多,业界公认的常用算法思想有8种,分别是枚举.递推.递归.分治.贪心.试探法.动态迭代和模拟.当然8种只是一个大概的划分,是一个"仁者见仁.智者见智"的问题. 其实这些 ...

  6. 数据结构(四) -- C语言版 -- 线性表的链式存储 - 循环链表

    文章目录 零.读前说明 一.循环链表的概述 二.循环链表的模型 2.1.包含头节点模型 2.2.不包含头节点模型 三.工程结构及简单测试案例 3.1.测试工程的目录结构 3.2.循环链表示例源码 3. ...

  7. 数据结构严蔚敏C语言版—线性表顺序存储结构(顺序表)C语言实现相关代码

    数据结构严蔚敏C语言版-线性表顺序存储结构(顺序表)C语言实现相关代码 1.运行环境 2.准备工作 1)项目构建 1>新建一个SeqList项目 2>新建两个文件Sources和Heade ...

  8. 线性表详解(静态链表、单链表、双向链表、循环链表)

    目录 申明 1. 线性表的定义 2. 线性表的抽象数据类型 3. 线性表的顺序存储结构 3. 1 顺序存储定义 3. 2 顺序存储方式 3. 3 数据长度与线性表长度区别 3. 4 地址计算方法 4. ...

  9. 逆置单链表c语言程序,(数据结构C语言版)顺序表和单链表的逆置

    <(数据结构C语言版)顺序表和单链表的逆置>由会员分享,可在线阅读,更多相关<(数据结构C语言版)顺序表和单链表的逆置(7页珍藏版)>请在人人文库网上搜索. 1.实验1-1顺序 ...

  10. c语言折半查找输出坐标,数据结构(C语言版)——有序表查找(折半查找)(代码版)...

    数据结构(C语言版)--有序表查找(折半查找)(代码版) 数据结构(C语言版)--有序表查找(折半查找)(代码版) #include #include #define ERROR 0 #define ...

最新文章

  1. mysql中transaction的实现
  2. combobox 怎么实现对listview的类别查询_通过 Django Pagination 实现简单分页
  3. 报错笔记:linux 命令行中的print输出内容无法重定向到文件中
  4. php 插入数据 不成功,thinkphp5连接oracle用insert插入数据失败
  5. 第一章: 新的结构化元素
  6. [转载]Oracle中动态SQL详解
  7. java图片工具类_Java 下载图片下载文件 工具类
  8. BT种子下载软件uTorrent Pro v3.5.5.45972
  9. ubuntu下海信Hisense E920 usb连接不上的处理与adb的连接
  10. smartdrv.exe 文件说明
  11. JDBC————PreparedStatement批处理
  12. python的布尔运算
  13. 视频会议软件中的协同文档技术
  14. 2.深入一点理解C源程序的编译过程
  15. Linux与数据结构 2019-4-14
  16. Tortoisegit 远端版本回退
  17. folly::ConcurrentSkipList 详解
  18. 怎样将语音转化为文字
  19. 8421码到5421码的转换_5421码8421码转化的逻辑表达式怎么样的?
  20. PPT打包成EXE常用方法

热门文章

  1. 过滤Android工程中多余的资源文件
  2. 小记安装python的MySQLdb模块
  3. React中的纯组件
  4. python中_new_和_init_的区别_Python 中的__new__和__init__的区别
  5. 少儿编程几种语言_您使用了几种编程语言?
  6. sre8 sre10_是什么使SRE出色?
  7. nginx mozilla_如何开始为Mozilla贡献力量
  8. c# c均值聚类及DBSCAN聚类
  9. 第三十五章 大唐工厂主
  10. Bootstrap3 按钮状态提示