前言
在前面介绍了线性表是什么,现在来讲述线性表的两种物理结构的第一种——顺序存储结构。
学习本文需要掌握一定的 C语言基础。
本部分内容参考程杰老师《大话数据结构》,青岛大学王卓老师的授课,王道考研公开课等 综合个人所学的总结笔记。
线性表的顺序存储结构
顺序表——用顺序存储的方式实现的线性表
顺序存储定义
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。
线性表的顺序存储示意图如下:

可以通过
sizeof(ElemType)
函数来获取数据元素的大小
顺序表的特点是表中元素的逻辑顺序与其物理顺序相同。
顺序表上的基本操作的实现
静态分配
静态分配,顾名思义,它的分配空间在一开始定义的时候就是固定的,代码示例:
#define MaxSize 10 //链表默认空间大小
typedef struct
{
ElemType data[MaxSize]; //静态空间大小
int length; //链表的长度
}SqList; //链表结构体
ElemType
需要看你具体使用的数据结构的类型此处
Sq
为sequence
的缩写
实现了顺序表静态分配表的初始化和插入操作,代码示例:
// 线性表的顺序表示
#include <stdio.h>
#define MaxSize 10 //链表默认空间大小
#define ElemType int
typedef struct
{
ElemType data[MaxSize]; //静态空间大小
int length; //链表的长度
}SqList; //链表结构体
//初始化表
void InitList(SqList sq){
for (int i = 0; i < MaxSize; i++)
{
sq.data[i] = 0; //初始化线性表的空间
}
sq.length = 0; //设定初始长度
}
//插入元素
void ListInsert(SqList *sq,int i,ElemType e){
if (i>sq->length+1)
{
printf("超出预定数组");
}else{
if (i==sq->length+1)
{
sq->data[i-1] = e;
}else{
for (int i = 0; i < (sq->length-i+1); i++)
{
sq->data[sq->length-i] = sq->data[sq->length - i - 1];
}
sq->data[i - 1] = e;
}
sq->length += 1;
printf("插入操作完成\n");
}
}
//输出线性表
void PrintList(SqList *sq){
for (int i = 0; i < sq->length; i++)
{
printf("%d \n",sq->data[i]);
}
}
int main(){
SqList sq; //声明顺序表
InitList(sq); //初始化顺序表
for (int i = 0; i < 5; i++)
{
sq.data[i] = i+1; //“违规”填入5个数据测试
}
sq.length = 5;
PrintList(&sq); //输出当前顺序表的内容
ListInsert(&sq, 4, 6); //在第4个插入整型6
PrintList(&sq); //输出插入后的结果
}
运行结果:

可以很明显的看出来,这种静态分配的方式的弊端在于其分配的内存空间在一开始是缺点的,使用起来很不灵活,所以引出下面的动态分配,通过动态的分配内存空间来合理运用内存。
动态分配
动态分配内存则需要使用两个函数malloc()
和free()
函数,这两个函数分别负责申请和释放内存,关于这两个函数的详细说明查看这篇文章:处理动态链表的函数。
使用如上函数需要引入
<malloc.h>
头文件
代码示例:
#include <stdio.h>
#include <malloc.h>
#define InitSize 10 //初始化表长度
typedef struct{
int *data; //表的开始地址
int maxsize; //表长度
int length; //表的数据长度
}SqList;
void IncreaseSize(SqList *sq, int size); //增加表长度函数原型声明
void InitList(SqList *sq); //初始化表函数原型声明
int main(){
SqList sq;
InitList(&sq); //初始化表
printf("当前表长度为:%d \n", sq.maxsize); //当前表长
IncreaseSize(&sq, 5); //增加表长
printf("当前表长度为:%d", sq.maxsize); //增加表长后的表长
}
//初始化顺序表
void InitList(SqList *sq){
sq->data = (int *)malloc(sizeof(int) * InitSize); //将申请的空间的头指针返回表
sq->maxsize = InitSize; //初始化表大小
sq->length = 0; //初始化表长度
}
//动态的增加表的长度
void IncreaseSize(SqList *sq,int size){
int * tempdata = sq->data; //交互旧数据到临时指针
sq->data = (int *)malloc(sizeof(int) * (sq->maxsize + size)); //开辟新内存
for (int i = 0; i < sq->length; i++)
{
sq->data[i] = tempdata[i]; //将旧数据复制到新内存上
}
sq->maxsize += size; //增加数据表长度
free(tempdata); //释放临时指针
}
运行结果:

插入操作
插入元素的时候,需要将插入的元素位置后的元素全部向后移动一位,代码示例:
//插入元素
bool ListInsert(SqList *sq,int i,ElemType e){
if (i<1||i>sq->length+1) //插入非法位置判断
return false;
if (sq->length>=sq->maxsize) //存储空间满
return false;
for (int j = sq->length; j >= i; j--) //插入元素
{
sq->data[j] = sq->data[j-1]; //元素位置后移一位
}
sq->data[i-1] = e; //插入元素
sq->length++; //增加顺序表的长度
return true; //返回操作成功
}
注:如果你使用
VS code
编译器报错存在未定义的bool
,则引入#include <stdbool.h>
删除操作
删除元素的时候,需要将删除元素位置后的元素全部前移一位,代码示例:
//删除元素
bool ListDelete(SqList *sq,int i){
if(i<1||i>sq->length) //判断删除位置是否合法
return false;
printf("删除元素 %d \n",sq->data[i-1]);
for (int j = i; j < sq->length; j++)
{
sq->data[j - 1] = sq->data[j]; //前移一位
}
sq->length--; //顺序表长度减一
return true; //返回操作成功
}
//删除测试
int main(){
SqList sq; //声明顺序表
InitList(sq); //初始化顺序表
for (int i = 0; i < 4; i++)
{
sq.data[i] = i+1; //非法传入8个参数
}
sq.length = 4;
PrintList(&sq);
if (ListDelete(&sq, 2)) //删除第五个位置的元素
{
printf("删除元素成功\n");
}else{
printf("删除元素失败,删除位置不合法");
}
PrintList(&sq); //输出删除后的元素列表
}
运行结果:

查找操作
按位查找
//查找操作——按位查找
ElemType GetElem(SqList *sq,int i){
if (i<1||i>sq->length)
return -1;
return sq->data[i - 1];
}
按值查找
//按值查找
ElemType LocateElem(SqList *sq,ElemType e){
for (int i = 0; i < sq->length; i++)
if (sq->data[i]==e)
return i+1;
return -1;
}
查找测试
int main(){
SqList sq; //声明顺序表
InitList(&sq); //初始化顺序表
for (int i = 0; i < 8; i++)
{
sq.data[i] = i+1; //非法传入8个参数
}
sq.length = 8;
PrintList(&sq);
printf("按位置查找元素值为 %d \n",GetElem(&sq, 2));
printf("按值查找元素位置为 %d",LocateElem(&sq, 3));
}
运行结果:

顺序表的特点
- 随机访问,即在 $O(1)$ 的时间内找到第 i 个元素
- 存储密度高,每个节点只存储数据元素
- 拓展容量不方便(即使是动态分配,也需要大量的时间拓展)
- 插入,删除操作不方便,需要移动大量的元素
线性表的链式存储结构
顺序表可以随时存取表中任意一个元素,它的存储和读取更为直观,但是对于插入和删除操作则需要移动大量元素。使用链式存储,不需要大量连续的存储地址,它通过指针来指示下一部分的存储位置,对于删除和插入操作来说,它不需要大量移动元素就可以完成。
链式存储结构根据其设计类型不同还可以分为:
- 单链表
- 双链表
- 循环链表
- 静态链表
前排提醒:下面代码中的
ElemType
,是我一开始定义的#define ElemType int
单链表
单链表的定义

单链表的基本操作实现
单链表的定义
单链表的每个结点包含两个部分:数据域和指针域,分别用来存储数据和指向下一个结点的地址。
而头结点只表示链表的地址,也就是链表本身的表示方式,其数据域负责存储链表的结点数量信息。
//单链表结点结构
typedef struct Node{
ElemType data;
struct Node *next;
}Node;
//单链表头结点
typedef struct SingleList
{
int n; //表示链表长度
Node *first; //链表的起始地址
}SingleList;
初始化链表
对创建的单链表进行初始化工作。
//初始化单链表
bool InitList(SingleList* L){
L->first = NULL; //头结点为NULL
L->n = 0; //结点数量初始化
return true;
}
插入结点
将新数据插入链表中,需要考虑插入的位序是否在链表中不存在,在插入过程中,需要给插入的数据创建专门的结点,然后将结点的指针域指向至其要插入的位序的下一个结点,再将其要插入位序的上一个结点指向新结点完成插入。代码示例:
注意:需要先将新结点指向后面再将插入点前的结点的指针域指向新指针吗,反之,则会出现断链
//按位序插入结点
bool InsertNode(SingleList* L,int locattion,ElemType data){
if (locattion > L->n+1 || locattion < 1)
{
printf("插入失败\n");
return false;
}else{
// Node* tempout = malloc(sizeof(Node)); //申请插入结点的内存空间
Node* tempout = malloc(sizeof(Node));
Node* tempin = (Node *)L; //临时结点
for (int i = 0; i < locattion-1; i++)
{
tempin = tempin->next; //获取插入结点的前一个结点
}
tempout->data = data; //插入结点包含的数据
tempout->next = tempin->next; //将插入结点的下一个指针指向原来指针指向的位置
tempin->next = tempout; //将插入位置前一个结点的指针指向插入结点
L->n++; //链表长度+1
printf("插入成功\n");
return true;
}
}
删除结点
思路:便利获取删除结点的前一个结点或者便利获取删除结点的前一个结点和删除结点,然后将删除结点的前一个结点的指针域指向删除的结点的指针域,即删除结点的下一个结点。代码示例:
//删除结点——按位序删除
bool DeleteNode(SingleList* L,int location){
if (location < 1 || location > L->n)
{
return false;
}
Node *temp = (Node*)L; //删除结点的前一个结点
Node *tempNext = (Node*)L; //删除结点
for (int i = 0; i < location; i++)
{
tempNext = tempNext->next; //获取删除结点
if(i<location-1)
temp = temp->next; //获取删除结点的前一个结点
}
temp->next = tempNext->next; //将删除结点的前一个结点的指针域指向删除结点的后一个结点
printf("删除位序(%d)成功,其数据为 %d\n", location, tempNext->data);
free(tempNext); //释放删除结点
L->n--; //表长度减一
return true;
}
输出链表
思路:遍历链表打印输出即可。
//输出链表
bool PrintList(SingleList* L){
if (L->first ==NULL)
{
printf("输出失败");
return false;
}else{
Node *temp = (Node*)L;
printf("链表的数据为:");
while (temp->next!=NULL)
{
temp = temp->next;
printf("%d", temp->data);
}
printf("\n");
return true;
}
}
链表查找——按位序查找
//单链表的查找——按位查找
bool GetElem(SingleList* L,int location){
if (location < 1 || location >L->n) //判断查找位序是否超出范围
{
return false;
}
Node * temp = (Node*)L;
for (int i = 0; i < location; i++)
{
temp = temp->next; //便利到指定位序获取结点
}
printf("该位序数据域为: %d \n", temp->data); //输出查找的位序结点的数据域
return true;
}
链表查找——按值查找
//单链表的查找——按值查找
bool GetElemLocation(SingleList* L,ElemType data){
Node * temp = (Node*)L; //起始位置为头结点
for (int i = 0; i < L->n; i++)
{
temp = temp->next; //移动下一个结点
if (temp->data ==data) //判断是否符合数据
{
printf("指定数据的位序为: %d\n", i+1);
return true;
}
}
return false; //遍历完成不存在该数据,返回false
}
建立单链表——尾插法
//单链表的建立——尾插法
bool List_TailInsert(SingleList* L){
Node *temp = (Node*)L;
int datas;
scanf("%d", &datas); //获取第一个输入的数值
while (datas!=9999) //取一个特殊值表示终止单链表数据的输入
{
Node *tempNext = malloc(sizeof(Node)); //申请一个新结点的内存空间
tempNext->data = datas; //新结点的数据域设定为输入的数据
temp->next = tempNext; //将结点指向新创建的结点
temp = temp->next; //结点向下走一个
L->n++; //表长+1
scanf("%d", &datas);
}
temp->next = NULL; //单链表最后的结点的指针域为NULL
return true;
}
建立单链表——头插法
//单链表的建立——头插法
bool List_HandInsert(SingleList* L){
int datas;
scanf("%d", &datas); //获取第一个输入的数值
while (datas!=9999) //取一个特殊值表示终止单链表数据的输入
{
Node *tempNext = malloc(sizeof(Node)); //为新数据结点开辟内存空间
tempNext->data = datas; //将输入的数据传递给新结点的数据域
tempNext->next = L->first; //新结点的指针域指向单链表的第一个结点
L->first = tempNext; //单链表头结点指向新结点
L->n++; //链表长度+1
scanf("%d", &datas); //获取第一个输入的数值
}
return true;
}
双链表
在单链表插入操作的时候,往往我们是没有办法直接获取一个结点的前驱结点,双链表就解决了这个问题,在定义结点的时候,不仅存储下一个结点的位置,还存储上一个结点的位置。
双链表(带头结点)
//结点结构体
typedef struct Node
{
ElemType data; //数据域
struct Node* before; //前结点指针
struct Node* next; //后结点指针
}Node,Dlist;
双链表建立——尾插法
//建立双链表——尾插法
bool List_TailInsert(Dlist* L){
Node* temp = L;
int data;
printf("请输入数值:");
scanf("%d", &data);
while (data != 9999)
{
Node* tempNext = (Node*)malloc(sizeof(Node)); //插入的新结点开辟内存空间
tempNext->data = data; //将数值放入结点的数据域
tempNext->next = temp->next; //新结点的后指针指向前一个结点的后指针
tempNext->before = temp; //新结点的前指针指向前一个结点
temp->next = tempNext; //前一个结点的后指针指向新结点
temp = temp->next; //结点光标移动到下一位
printf("请输入数值:");
scanf("%d", &data);
}
return true;
}
*L
的用法详见C指针运算符
输出双链表内容
//输出链表
bool PrintList(Dlist* L) {
if(L->next == NULL){
printf("链表为空");
return false;
}
Node* temp = L->next;
printf("链表内容为:");
printf("%d", temp->data);
while (temp->next != NULL)
{
temp = temp->next;
printf("%d", temp->data);
}
printf("\n");
return true;
}
双链表(不带头结点)
//结点结构体
typedef struct Node
{
ElemType data; //数据域
struct Node* before; //前结点指针
struct Node* next; //后结点指针
}Node, * Dlist;
双链表建立——头插法
//头插法建立双链表
bool List_TailInsert(Dlist* L) {
int data;
printf("请输入数值:");
scanf("%d", &data);
while (data != 9999)
{
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = *L;
newNode->before = NULL;
*L = newNode;
printf("请输入数值:");
scanf("%d", &data);
}
return true;
}
输出双链表的内容
//输出链表
bool PrintList(Dlist* L) {
if (L == NULL) {
printf("链表为空");
return false;
}
Node* temp = *L;
printf("链表内容为:");
printf("%d", temp->data);
while (temp->next != NULL)
{
temp = temp->next;
printf("%d", temp->data);
}
printf("\n");
return true;
}
循环链表
循环链表是单链表的最后一个结点的指针域指向头结点。

循环链表的定义
此处借用单链表定义的内容,代码示例:
//循环链表结点结构
typedef struct Node{
ElemType data;
struct Node *next;
}Node;
//循环表头结点
typedef struct SingleList
{
int n; //表示链表长度
Node *first; //链表的起始地址
}SingleList;
循环链表初始化
//循环链表初始化
bool InitList(SingleList * L){
L->first = (Node*)L;
L->n = 0;
return true;
}
循环链表的建立——头插法
//循环链表建立——头插法
bool List_headInsert(SingleList *L){
int data;
printf("请输入数值:");
scanf("%d", &data);
while (data !=9999)
{
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = data; //数据赋值到新结点
newNode->next = L->first; //新结点指向头结点的指针域
L->first = newNode; //头结点指向新结点
L->n++;
printf("请输入数值:");
scanf("%d", &data);
}
return true;
}
输出双链表
//输出循环链表
bool printList(SingleList * L){
if(L->n<1)
return false;
Node *temp = L->first;
printf("循环链表数值为:");
for (int i = 0; i < L->n; i++)
{
printf("%d", temp->data);
temp = temp->next;
}
return true;
}
静态链表
在一些没有指针的语言中,前辈们想到的用静态链表的方式来实现链表的功能的办法。

静态链表的定义
#define MaxSize 10
#define ElemType int
//静态链表结构
typedef struct Node{
ElemType data;
int next;
}Node,StaicList[MaxSize];
静态链表的操作
不想写啊啊啊,空着了
End
如上,线性表部分结束,最后呈现单链表,双链表的整合代码,代码不全,仅做示例。
-
单链表
#include <stdio.h> #include <stdbool.h> #include <malloc.h> #define ElemType int //单链表结点结构 typedef struct Node{ ElemType data; struct Node *next; }Node; //单链表头结点 typedef struct SingleList { int n; //表示链表长度 Node *first; //链表的起始地址 }SingleList; bool InitList(SingleList *L); //初始化表 bool InsertNode(SingleList *L, int locattion, ElemType data); //按位序插入结点 bool PrintList(SingleList *L); //输出链表 bool DeleteNode(SingleList *L, int location); //删除结点——按位序 bool GetElem(SingleList* L,int location); //链表查找——按位序 bool GetElemLocation(SingleList *L, ElemType data); //链表查找——按值 bool List_TailInsert(SingleList *L); //建立单链表——尾插法 bool List_HandInsert(SingleList *L); //建立单链表——头插法 //程序入口点 int main(){ SingleList singleList; InitList(&singleList); List_HandInsert(&singleList); PrintList(&singleList); InsertNode(&singleList, 2, 6); PrintList(&singleList); } //初始化单链表 bool InitList(SingleList* L){ L->first = NULL; L->n = 0; return true; } //按位序插入结点 bool InsertNode(SingleList* L,int locattion,ElemType data){ if (locattion > L->n+1 || locattion < 1) { printf("插入失败\n"); return false; }else{ // Node* tempout = malloc(sizeof(Node)); //申请插入结点的内存空间 Node* tempout = malloc(sizeof(Node)); Node* tempin = (Node *)L; //临时结点 for (int i = 0; i < locattion-1; i++) { tempin = tempin->next; //获取插入结点的前一个结点 } tempout->data = data; //插入结点包含的数据 tempout->next = tempin->next; //将插入结点的下一个指针指向原来指针指向的位置 tempin->next = tempout; //将插入位置前一个结点的指针指向插入结点 L->n++; printf("插入成功\n"); return true; } } //输出链表 bool PrintList(SingleList* L){ if (L->first ==NULL) { printf("输出失败"); return false; }else{ Node *temp = (Node*)L; printf("链表的数据为:"); while (temp->next!=NULL) { temp = temp->next; printf("%d", temp->data); } printf("\n"); return true; } } //删除结点——按位序删除 bool DeleteNode(SingleList* L,int location){ if (location < 1 || location > L->n) { return false; } Node *temp = (Node*)L; //删除结点的前一个结点 Node *tempNext = (Node*)L; //删除结点 for (int i = 0; i < location; i++) { tempNext = tempNext->next; //获取删除结点 if(i<location-1) temp = temp->next; //获取删除结点的前一个结点 } temp->next = tempNext->next; //将删除结点的前一个结点的指针域指向删除结点的后一个结点 printf("删除位序(%d)成功,其数据为 %d\n", location, tempNext->data); free(tempNext); //释放删除结点 L->n--; //表长度减一 return true; } //单链表的查找——按位查找 bool GetElem(SingleList* L,int location){ if (location < 1 || location >L->n) //判断查找位序是否超出范围 { return false; } Node * temp = (Node*)L; for (int i = 0; i < location; i++) { temp = temp->next; //便利到指定位序获取结点 } printf("该位序数据域为: %d \n", temp->data); //输出查找的位序结点的数据域 return true; } //单链表的查找——按值查找 bool GetElemLocation(SingleList* L,ElemType data){ Node * temp = (Node*)L; //起始位置为头结点 for (int i = 0; i < L->n; i++) { temp = temp->next; //移动下一个结点 if (temp->data ==data) //判断是否符合数据 { printf("指定数据的位序为: %d\n", i+1); return true; } } return false; //遍历完成不存在该数据,返回false } //单链表的建立——尾插法 bool List_TailInsert(SingleList* L){ Node *temp = (Node*)L; int datas; scanf("%d", &datas); //获取第一个输入的数值 while (datas!=9999) //取一个特殊值表示终止单链表数据的输入 { Node *tempNext = malloc(sizeof(Node)); //申请一个新结点的内存空间 tempNext->data = datas; //新结点的数据域设定为输入的数据 temp->next = tempNext; //将结点指向新创建的结点 temp = temp->next; //结点向下走一个 L->n++; //表长+1 scanf("%d", &datas); } temp->next = NULL; //单链表最后的结点的指针域为NULL return true; } //单链表的建立——头插法 bool List_HandInsert(SingleList* L){ int datas; scanf("%d", &datas); //获取第一个输入的数值 while (datas!=9999) //取一个特殊值表示终止单链表数据的输入 { Node *tempNext = malloc(sizeof(Node)); //为新数据结点开辟内存空间 tempNext->data = datas; //将输入的数据传递给新结点的数据域 tempNext->next = L->first; //新结点的指针域指向单链表的第一个结点 L->first = tempNext; //单链表头结点指向新结点 L->n++; //链表长度+1 scanf("%d", &datas); //获取第一个输入的数值 } return true; }
-
双链表
#include <stdio.h> #include <stdbool.h> #include <malloc.h> #define ElemType int //结点结构体 typedef struct Node { ElemType data; //数据域 struct Node* before; //前结点指针 struct Node* next; //后结点指针 }Node,Dlist; bool List_TailInsert(Dlist* L); //建立双链表——尾插法 bool PrintList(Dlist* L); //输出链表 bool InitSingleList(Dlist* L); //初始化链表 //程序入口 int main() { Dlist L; InitSingleList(&L); List_TailInsert(&L); PrintList(&L); } //链表初始化 bool InitSingleList(Dlist* L) { L->before = NULL; L->next = NULL; return true; } //建立双链表——尾插法 bool List_TailInsert(Dlist* L){ Node* temp = L; int data; printf("请输入数值:"); scanf("%d", &data); while (data != 9999) { Node* tempNext = (Node*)malloc(sizeof(Node)); //插入的新结点开辟内存空间 tempNext->data = data; //将数值放入结点的数据域 tempNext->next = temp->next; //新结点的后指针指向前一个结点的后指针 tempNext->before = temp; //新结点的前指针指向前一个结点 temp->next = tempNext; //前一个结点的后指针指向新结点 temp = temp->next; //结点光标移动到下一位 printf("请输入数值:"); scanf("%d", &data); } return true; } //输出链表 bool PrintList(Dlist* L) { if(L->next == NULL){ printf("链表为空"); return false; } Node* temp = L->next; printf("链表内容为:"); printf("%d", temp->data); while (temp->next != NULL) { temp = temp->next; printf("%d", temp->data); } printf("\n"); return true; }
Q.E.D.