第二部分、线性表详解:数据结构线性表10分钟入门
线性表,数据结构中最简单的一种存储结构,专门用于存储逻辑关系为"一对一"的数据。
线性表,基于数据在实际物理空间中的存储状态,又可细分为顺序表(顺序存储结构)和链表(链式存储结构)。
本章还会讲解顺序表和链表的结合体——静态链表,不仅如此,还会涉及循环链表、双向链表、双向循环链表等链式存储结构。
三、顺序表的基本操作(C语言详解版)
我们学习了《二、顺序表(顺序存储结构)及初始化详解》一节,本节学习有关顺序表的一些基本操作,以及如何使用 C 语言实现它们。
1、顺序表插入元素
向已有顺序表中插入数据元素,根据插入位置的不同,可分为以下 3 种情况:
- 插入到顺序表的表头;
- 在表的中间位置插入元素;
- 尾随顺序表中已有元素,作为顺序表中的最后一个元素;
虽然数据元素插入顺序表中的位置有所不同,但是都使用的是同一种方式去解决,即:通过遍历,找到数据元素要插入的位置,然后做如下两步工作:
- 将要插入位置元素以及后续的元素整体向后移动一个位置;
- 将元素放到腾出来的位置上;
例如,在 {1,2,3,4,5}
的第 3 个位置上插入元素 6,实现过程如下:
- 遍历至顺序表存储第 3 个数据元素的位置,如图 1 所示:
图 1 找到目标元素位置
- 将元素 3 以及后续元素 4 和 5 整体向后移动一个位置,如图 2 所示:
图 2 将插入位置腾出
- 将新元素 6 放入腾出的位置,如图 3 所示:
图 3 插入目标元素
因此,顺序表插入数据元素的 C 语言实现代码如下:
//插入函数,其中,elem为插入的元素,add为插入到顺序表的位置
table addTable(table t,int elem,int add)
{
//判断插入本身是否存在问题(如果插入元素位置比整张表的长度+1还大(如果相等,是尾随的情况),或者插入的位置本身不存在,程序作为提示并自动退出)
if (add>t.length+1||add<1) {
printf("插入位置有问题\n");
return t;
}
//做插入操作时,首先需要看顺序表是否有多余的存储空间提供给插入的元素,如果没有,需要申请
if (t.length==t.size) {
t.head=(int *)realloc(t.head, (t.size+1)*sizeof(int));
if (!t.head) {
printf("存储分配失败\n");
return t;
}
t.size+=1;
}
//插入操作,需要将从插入位置开始的后续元素,逐个后移
for (int i=t.length-1; i>=add-1; i--) {
t.head[i+1]=t.head[i];
}
//后移完成后,直接将所需插入元素,添加到顺序表的相应位置
t.head[add-1]=elem;
//由于添加了元素,所以长度+1
t.length++;
return t;
}
注意,动态数组额外申请更多物理空间使用的是 realloc 函数。并且,在实现后续元素整体后移的过程,目标位置其实是有数据的,还是 3,只是下一步新插入元素时会把旧元素直接覆盖。
2、顺序表删除元素
从顺序表中删除指定元素,实现起来非常简单,只需找到目标元素,并将其后续所有元素整体前移 1 个位置即可。
后续元素整体前移一个位置,会直接将目标元素删除,可间接实现删除元素的目的。
例如,从 {1,2,3,4,5}
中删除元素 3 的过程如图 4 所示:
图 4 顺序表删除元素的过程示意图
因此,顺序表删除元素的 C 语言实现代码为:
table delTable(table t,int add){
if (add>t.length || add<1) {
printf("被删除元素的位置有误\n");
return t;
}
//删除操作
for (int i=add; i<t.length; i++) {
t.head[i-1]=t.head[i];
}
t.length--;
return t;
}
3、顺序表查找元素
顺序表中查找目标元素,可以使用多种查找算法实现,比如说《第九部分:三:二分查找(折半查找)算法详解(C语言实现)》、插值查找算法等。
这里,我们选择《第九部分:二:顺序查找算法详解(包含C语言实现代码)》,具体实现代码为:
//查找函数,其中,elem表示要查找的数据元素的值
int selectTable(table t,int elem){
for (int i=0; i<t.length; i++) {
if (t.head[i]==elem) {
return i+1;
}
}
return -1;//如果查找失败,返回-1
}
4、顺序表更改元素
顺序表更改元素的实现过程是:
- 找到目标元素;
- 直接修改该元素的值;
顺序表更改元素的 C 语言实现代码为:
//更改函数,其中,elem为要更改的元素,newElem为新的数据元素
table amendTable(table t,int elem,int newElem){
int add=selectTable(t, elem);
t.head[add-1]=newElem;//由于返回的是元素在顺序表中的位置,所以-1就是该元素在数组中的下标
return t;
}
以上是顺序表使用过程中最常用的基本操作,这里给出本节完整的实现代码:
#include <stdio.h>
#include <stdlib.h>
#define Size 5
typedef struct Table{
int * head;
int length;
int size;
}table;
table initTable(){
table t;
t.head=(int*)malloc(Size*sizeof(int));
if (!t.head) {
printf("初始化失败\n");
exit(0);
}
t.length=0;
t.size=Size;
return t;
}
table addTable(table t,int elem,int add)
{
if (add>t.length+1||add<1) {
printf("插入位置有问题\n");
return t;
}
if (t.length>=t.size) {
t.head=(int *)realloc(t.head, (t.size+1)*sizeof(int));
if (!t.head) {
printf("存储分配失败\n");
}
t.size+=1;
}
for (int i=t.length-1; i>=add-1; i--) {
t.head[i+1]=t.head[i];
}
t.head[add-1]=elem;
t.length++;
return t;
}
table delTable(table t,int add){
if (add>t.length || add<1) {
printf("被删除元素的位置有误\n");
return t;
}
for (int i=add; i<t.length; i++) {
t.head[i-1]=t.head[i];
}
t.length--;
return t;
}
int selectTable(table t,int elem){
for (int i=0; i<t.length; i++) {
if (t.head[i]==elem) {
return i+1;
}
}
return -1;
}
table amendTable(table t,int elem,int newElem){
int add=selectTable(t, elem);
t.head[add-1]=newElem;
return t;
}
void displayTable(table t){
for (int i=0;i<t.length;i++) {
printf("%d ",t.head[i]);
}
printf("\n");
}
int main(){
table t1=initTable();
for (int i=1; i<=Size; i++) {
t1.head[i-1]=i;
t1.length++;
}
printf("原顺序表:\n");
displayTable(t1);
printf("删除元素1:\n");
t1=delTable(t1, 1);
displayTable(t1);
printf("在第2的位置插入元素5:\n");
t1=addTable(t1, 5, 2);
displayTable(t1);
printf("查找元素3的位置:\n");
int add=selectTable(t1, 3);
printf("%d\n",add);
printf("将元素3改为6:\n");
t1=amendTable(t1, 3, 6);
displayTable(t1);
return 0;
}
程序运行结果为:
原顺序表:
1 2 3 4 5
删除元素1:
2 3 4 5
在第2的位置插入元素5:
2 5 3 4 5
查找元素3的位置:
3
将元素3改为6:
2 5 6 4 5
四、什么是单链表,链式存储结构详解
前面详细地介绍了《二、顺序表(顺序存储结构)及初始化详解》,本节给大家介绍另外一种线性存储结构——链表。
链表,别名链式存储结构或单链表,用于存储逻辑关系为 "一对一" 的数据。与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。
例如,使用链表存储 {1,2,3}
,数据的物理存储状态如图 1 所示:
图 1 链表随机存储数据
我们看到,图 1 根本无法体现出各数据之间的逻辑关系。对此,链表的解决方案是,每个数据元素在存储时都配备一个指针,用于指向自己的直接后继元素。如图 2 所示:
图 2 各数据元素配备指针
像图 2 这样,数据元素随机存储,并通过指针表示数据之间逻辑关系的存储结构就是链式存储结构。
1、链表的节点
从图 2 可以看到,链表中每个数据的存储都由以下两部分组成:
- 数据元素本身,其所在的区域称为数据域;
- 指向直接后继元素的指针,所在的区域称为指针域;
即链表中存储各数据元素的结构如图 3 所示:
图 3 节点结构
图 3 所示的结构在链表中称为节点。也就是说,链表实际存储的是一个一个的节点,真正的数据元素包含在这些节点中,如图 4 所示:
图 4 链表中的节点
因此,链表中每个节点的具体实现,需要使用 C 语言中的结构体,具体实现代码为:
typedef struct Link{
char elem; //代表数据域
struct Link * next; //代表指针域,指向直接后继元素
}link; //link为节点名,每个节点都是一个 link 结构体
提示,由于指针域中的指针要指向的也是一个节点,因此要声明为 Link 类型(这里要写成 struct Link*
的形式)。
2、头节点,头指针和首元节点
其实,图 4 所示的链表结构并不完整。一个完整的链表需要由以下几部分构成:
- 头指针:一个普通的指针,它的特点是永远指向链表第一个节点的位置。很明显,头指针用于指明链表的位置,便于后期找到链表并使用表中的数据;
- 节点:链表中的节点又细分为头节点、首元节点和其他节点:
- 头节点:其实就是一个不存任何数据的空节点,通常作为链表的第一个节点。对于链表来说,头节点不是必须的,它的作用只是为了方便解决某些实际问题;
- 首元节点:由于头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。首元节点只是对链表中第一个存有数据节点的一个称谓,没有实际意义;
- 其他节点:链表中其他的节点;
因此,一个存储 {1,2,3}
的完整链表结构如图 5 所示:
图 5 完整的链表示意图
注意:链表中有头节点时,头指针指向头节点;反之,若链表中没有头节点,则头指针指向首元节点。
明白了链表的基本结构,下面我们来学习如何创建一个链表。
3、链表的创建(初始化)
创建一个链表需要做如下工作:
- 声明一个头指针(如果有必要,可以声明一个头节点);
- 创建多个存储数据的节点,在创建的过程中,要随时与其前驱节点建立逻辑关系;
例如,创建一个存储 {1,2,3,4}
且无头节点的链表,C 语言实现代码如下:
link * initLink(){
link * p=NULL;//创建头指针
link * temp = (link*)malloc(sizeof(link));//创建首元节点
//首元节点先初始化
temp->elem = 1;
temp->next = NULL;
p = temp;//头指针指向首元节点
//从第二个节点开始创建
for (int i=2; i<5; i++) {
//创建一个新节点并初始化
link *a=(link*)malloc(sizeof(link));
a->elem=i;
a->next=NULL;
//将temp节点与新建立的a节点建立逻辑关系
temp->next=a;
//指针temp每次都指向新链表的最后一个节点,其实就是 a节点,这里写temp=a也对
temp=temp->next;
}
//返回建立的节点,只返回头指针 p即可,通过头指针即可找到整个链表
return p;
}
如果想创建一个存储 {1,2,3,4}
且含头节点的链表,则 C 语言实现代码为:
link * initLink(){
link * p=(link*)malloc(sizeof(link));//创建一个头结点
link * temp=p;//声明一个指针指向头结点,
//生成链表
for (int i=1; i<5; i++) {
link *a=(link*)malloc(sizeof(link));
a->elem=i;
a->next=NULL;
temp->next=a;
temp=temp->next;
}
return p;
}
我们只需在主函数中调用 initLink 函数,即可轻松创建一个存储 {1,2,3,4}
的链表,C 语言完整代码如下:
#include <stdio.h>
#include <stdlib.h>
//链表中节点的结构
typedef struct Link{
int elem;
struct Link *next;
}link;
//初始化链表的函数
link * initLink();
//用于输出链表的函数
void display(link *p);
int main() {
//初始化链表(1,2,3,4)
printf("初始化链表为:\n");
link *p=initLink();
display(p);
return 0;
}
link * initLink(){
link * p=NULL;//创建头指针
link * temp = (link*)malloc(sizeof(link));//创建首元节点
//首元节点先初始化
temp->elem = 1;
temp->next = NULL;
p = temp;//头指针指向首元节点
for (int i=2; i<5; i++) {
link *a=(link*)malloc(sizeof(link));
a->elem=i;
a->next=NULL;
temp->next=a;
temp=temp->next;
}
return p;
}
void display(link *p){
link* temp=p;//将temp指针重新指向头结点
//只要temp指针指向的结点的next不是Null,就执行输出语句。
while (temp) {
printf("%d ",temp->elem);
temp=temp->next;
}
printf("\n");
}
程序运行结果为:
初始化链表为:
1 2 3 4
注意,如果使用带有头节点创建链表的方式,则输出链表的 display 函数需要做适当地修改:
void display(link *p){
link* temp=p;//将temp指针重新指向头结点
//只要temp指针指向的结点的next不是Null,就执行输出语句。
while (temp->next) {
temp=temp->next;
printf("%d",temp->elem);
}
printf("\n");
}