紫薇星上的数据结构(2)

这部分主要整理一下线性表的知识点,线性表分为顺序表和链表,同时会有一些代码来实现概念。


2.1线性表抽象数据结构

线性表的定义

零个或多个数据元素的有限序列就可以说是线性表,比如学生名单,十二生肖等都是符合线性表定义的。

线性表的特点

线性表是一个序列,整个表中的数据元素之间是有序的;数据元素之间是一对一的关系;具有有限性,线性表的数据元素个数是有限的;零个数据元素的有限序列又被称为空表。

线性表常见的操作:创建和初始化;查找;插入;删除;清空。简单来说就是:增、删、改、查。

接下来我们整理一下线性表的定义:

ADT 线性表(SequenceList)
Data
    1、线性表的数据元素是一个集合{a_1,a_2,a_3,...,a_n}
       数据元素的类型DataType(int,double,或自定义)
    2、除了第一个元素a_1之外,每个元素有且只有一个直接的前驱元素。
    3、除了最后一个元素a_n之外,每个元素有且只有一个直接的后继元素。
    4、每个数据元素之间的关系是一对一的关系。
Operation
    InitList(*List)  初始化线性表:创建一个新的线性表List
    InsertElement(*List, index, elem)    在线性表List的index下标处插入元素elem
    DeleteElement(*List, index, *elem)   删除线性表List的index下标处的元素,并返回删除元素指针*elem
    GetLength(*List)   返回线性表List的长度
    IsEmpty(*List)     检查是否为空线性表
    ClearList(*List)   清楚线性表
    GetElement(*List, index, elem)       返回线性表List的index下标处元素elem
endADT

这就是线性表的基本定义方式了。

2.2顺序表

顺序存储结构的线性表,就是顺序表。线性表的顺序存储结构示意图如下:

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。看起来是不是有点像数组?没错,这其实是顺序表的底层原理。当然刚才我们展示了线性表的定义,真正使用程序来实现的时候我们选择C语言来实现。

// 1、我们首先要定义线性表中的最大存储空间
#define MAX_SIZE 255

// 2、线性表中需要有同一类型的元素集合
typedef int ElemType; // 这个地方使用ElemType来代替 int,只是代替名称,实质还是 int
typedef struct{ //这个地方使用ElementType来定义这个struct,这是我们自己定义的类型
    int id; //每个元素可能都有id
    char *name; //每个元素可能都有名字
}ElementType; //我们可以在这里存储任何的类型,可以是游戏中的某一个角色,也可能是某个账户的信息

// 3、定义顺序表结构
typedef struct{
    ElementType datas[MAX_SIZE]; //这里表示定义一个ElementType的数组datas,长度为MAX_SIZE
    int length; //这个length表示保存当前线性表的长度
};

上面这段代码描述了线性表的顺序存储结构需要的三个属性:

  • 储存空间的起始位置:数组datas的存储位置;
  • 线性表的最大存储容量:MAX_SIZE;
  • 线性表的当前长度:length。

地址计算方法

第 1 个元素的内存地址 = 数组的内存地址 + 第一个元素的下标 0;

第 2 个元素的内存地址 = 数组的内存地址 + 第一个元素的下标 1;

第 i 个元素的内存地址 = 数组的内存地址 + 第一个元素的下标 ( i - 1 );

第( i + 1 )个元素的内存地址 = 数组的内存地址 + 第一个元素的下标 i ;

第 n 个元素的内存地址 = 数组的内存地址 + 第一个元素的下标 ( n - 1 );

在上面的代码中,datas为存储位置,那么访问第一个数组元素的时候就要用 *(datas + 0);访问第二个数组元素的时候就要用 *(datas + 1);访问第 n 个数组元素的时候就要用 *(datas +  n - 1)。

后面我们还会经常用到两个单词:position、index,position代表的是位置,从 1 开始;index代表的是下标,从 0 开始。

2.3顺序表的算法

插入算法

首先我们打开编辑器,新建一个项目,然后我们建一个头文件:DataElement.h,这个头文件是用来定义数据元素和数据类型的。创建完成之后界面应该是这样的:

紫薇星上的数据结构(2)_第1张图片

然后我们就可以在DataElements里面定义数据元素了:

#ifndef DATAELEMENT_H_INCLUDED
#define DATAELEMENT_H_INCLUDED
#define MAX_SIZE 255
//1、定义数据元素
//typedef int ElementType;
typedef struct{
    int id;
    char *name;
}ElementType;

#endif // DATAELEMENT_H_INCLUDED

可以使用我们刚才讲的两种定义,上面代码使用的是第二种。现在我们来定义顺序表:

//2、定义顺序表结构
typedef struct{
    ElementType datas[MAX_SIZE];
    int length;
}SeqList;

这样子顺序表就定义完毕了:

#ifndef DATAELEMENT_H_INCLUDED
#define DATAELEMENT_H_INCLUDED
#define MAX_SIZE 255
//1、定义数据元素
typedef int ElementType;
typedef struct{
    int id;
    char *name;
}ElementType;

//2、定义顺序表结构
typedef struct{
    ElementType datas[MAX_SIZE];
    int length;
}SeqList;
#endif // DATAELEMENT_H_INCLUDED

现在我们来写顺序表的操作,首先还是新建一个头文件SequenceList.h,然后就可以在里面添加操作了,也就是我们上面的Operation:

#ifndef SEQUENCELIST_H_INCLUDED
#define SEQUENCELIST_H_INCLUDED

#include 
#include 
#include "DataElement.h"

//初始化顺序表,三个参数分别表示:要初始化的顺序表;要初始化添加的元素内容数组;初始化时添加的元素个数。
void InitList(SeqList *seqList, ElementType *elemArray, int length);
//向顺序表中插入元素,三个参数分别表示:要插入的顺序表;插入元素处的下标;要插入的元素。
void InsertElement(SeqList *seqList, int index, ElementType *element);
//方便起见,还要打印顺序表中内容
void PrintList(SeqList *seqList);

#endif // SEQUENCELIST_H_INCLUDED

然后我们为了实现插入的操作,还要再新建一个.c文件:SequenceList.c,这个文件用来实现刚才所写的插入操作:

#include "SequenceList.h"

//初始化顺序表,三个参数分别表示:要初始化的顺序表;要初始化添加的元素内容数组;初始化时添加的元素个数。
void InitList(SeqList *seqList, ElementType *elemArray, int length){

}

//向顺序表中插入元素,三个参数分别表示:要插入的顺序表;插入元素处的下标;要插入的元素。
void InsertElement(SeqList *seqList, int index, ElementType *element){

}

//方便起见,还要打印顺序表中内容
void PrintList(SeqList *seqList){

}

现在我们来思考之前说的如何插入,将元素X插入到顺序表(a_1,a_2,a_3,...,a_n)中下标为 i 的位置,那么就会发生以下操作:

  • 下标为 i 的数据以及 i 以后的数据全部向后移动一位(如果 i 是最后一位那就不需要移动)
  • 将下标 i 的位置放入数据元素X,i + 1 的位置放入元素 ai+1,以此类推直到 i = n 的时候位置上为 an。

这里要注意:

  • 插入元素后顺序表长度为n+1。
  • 插入元素后,最后一个元素的下标为 n。
  • C语言数组实现时,顺序表长度不能超过它的最大长度。

我们在方法中写好插入操作:

//向顺序表中插入元素,三个参数分别表示:要插入的顺序表;插入元素处的下标;要插入的元素。
void InsertElement(SeqList *seqList, int index, ElementType element){
    //1、验证插入元素后元素空间是否超过最大长度MAX_SIZE
    if(seqList->length + 1 >= MAX_SIZE){
        printf("数组已满,插入失败!\n");
        return;
    }
    //2、验证index的值是否合法,应该是0-MAX_SIZE之间
    if(index < 0 || index > MAX_SIZE - 1){
        printf("只允许在下标范围内插入元素!【0,%d】", MAX_SIZE - 1);
        return;
    }
    //3、插入的index必须小于length
    if(index > seqList->length){
        printf("插入的下标超过了数组最大长度,插入失败!");
        return;
    }
    //4、从第length - 1 个下标元素开始,到index,前面的元素赋值给后面的元素
    //在C89标准中不允许在for中直接定义变量
    //C99之后就允许了
    for(int i = seqList->length - 1; i >= index; i--){
        seqList->datas[i + 1] = seqList->datas[i];
    }
    //5、将插入的元素赋值给下标为index的元素
    seqList->datas[index] = element;
    //6、顺序表总长度+1,这是非常容易漏掉的地方!
    seqList->length++;
}

现在我们要开始写初始化操作,写好初始化操作之后就可以插入了:

//初始化顺序表,三个参数分别表示:要初始化的顺序表;要初始化添加的元素内容数组;初始化时添加的元素个数。
void InitList(SeqList *seqList, ElementType *elemArray, int length){
    if(length > MAX_SIZE){
        printf("超出了数组最大容量,初始化失败!");
        return;
    }
    seqList->length = 0;
    for(int i = 0; i < length; i++){
        //每次循环都在下标为i的位置插入一个元素
        InsertElement(seqList, i, elemArray[i]);
    }
}

然后我们写一下打印操作来验证是否成功:

//方便起见,还要打印顺序表中内容
void PrintList(SeqList *seqList){
    for(int i = 0; i < seqList->length; i++){
        printf("%d\t%s\n",seqList->datas[i].id, seqList->datas[i].name);
    }
}

之后就可以进行实现了,现在我们回到main.c:

#include 
#include 
#include "DataElement.h"

ElementType dataArray[] = {
    {1, "钢铁侠"},
    {2, "美国队长"},
    {3, "紫薇一号"},
    {4, "紫薇二号"},
    {5, "紫薇三号"}
};

void TestSequenceList();

int main(){
    //printf("Hello world!\n");
    TestSequenceList();
    return 0;
}

void TestSequenceList(){
    SeqList seqList;
    InitList(&seqList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
    PrintList(&seqList);
}

建立一个数组和一个方法,数组用来保存数据,方法用来实现操作,写好之后编译通过,运行结果如下:

1       钢铁侠
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号

Process returned 0 (0x0)   execution time : 0.043 s
Press any key to continue.

这就是顺序表的插入算法。

删除算法

与插入操作相同,首先要在SeqyenceList.h头文件中添加删除操作:

//删除顺序表中指定下标的元素,参数分别表示:要删除的顺序表;要删除元素的下标。
ElementType *DeleteElement(SeqList *seqList, int index);

这个时候我们就要分析如何删除,将元素X在顺序表(a_1,a_2,a_3,...,a_n)中下标为 i 的位置删除,那么就会发生以下操作:

  • 下标为 i 以后的数据全部向前移动一位(如果 i 是最后一位那就不需要移动)
  • 将顺序表长度减一,i  的位置放入元素 ai+1,以此类推直到 i = n - 1 的时候位置上为 an。

这里要注意:

  • 插入元素后顺序表长度为n。
  • 删除元素后,最后一个元素将被直接删除。
  • 被删除的元素要先找到保存以便返回。
  • C语言数组实现时,顺序表长度不能超过它的最大长度。

所以我们还应该在SeqyenceList.h头文件中添加查找操作:

ElementType *GetElement(SeqList *seqList, int index);

然后来到SeqyenceList.c文件中,添加查找操作:

ElementType *GetElement(SeqList *seqList, int index){
    if(index < 0 || index > MAX_SIZE){
        //同样的先进行判断
        printf("下标越界,查找失败!");
        return NULL;
    }
    ElementType *element; //要查找的元素
    element = &seqList->datas[index];
}

我们将查找到的元素放在element中,然后来写删除操作:

//删除顺序表中指定下标的元素,参数分别表示:要删除的顺序表;要删除元素的下标。
ElementType *DeleteElement(SeqList *seqList, int index){
    if(index < 0 || index > MAX_SIZE){
        //同样的先进行判断
        printf("下标越界,删除失败!");
        return NULL;
    }
    //1、找到要删除的元素,保存起来以便返回.(保存的是已删除元素的副本)
    ElementType *deleteElement = (ElementType*)malloc(sizeof(ElementType)); //要删除的元素
    //这里应该单独定义并调用查找函数,返回要删除元素的指针
    *deleteElement = *GetElement(seqList, index);
    //2、从指定位置删除,后面一个元素赋值给前面的一个元素
    for(int i = index; i < seqList->length - 1; i++){
        seqList->datas[i] = seqList->datas[i + 1];
    }
    //3、顺序表长度-1
    seqList->length --;
    return deleteElement;//建议使用完毕后进行ree,否则会造成内存泄漏。
}

这时我们将要查找到的元素分配空间,备份为要删除的元素,然后使用for循环删除,但其实并没有真正删除,只是将要删除的元素从顺序表中拿出来了,备份在deleteElement中,但由于使用了新的内存空间来储存,所以操作完成后可以free来释放内存。现在我们来看一下实际效果怎么样:

void TestSequenceList(){
    SeqList seqList;
    ElementType *deleteElement;
    InitList(&seqList, dataArray, sizeof(dataArray) / sizeof(dataArray[0]));
    printf("初始化后:\n");
    PrintList(&seqList);
    deleteElement = DeleteElement(&seqList, 3);
    printf("删除后:\n");
    PrintList(&seqList);
    printf("被删除的元素:\n");
    printf("%d\t%s\n",deleteElement->id, deleteElement->name);
    free(deleteElement);//一定要记得释放内存
}

编译通过,运行结果如下:

初始化后:
1       钢铁侠
2       美国队长
3       紫薇一号
4       紫薇二号
5       紫薇三号
删除后:
1       钢铁侠
2       美国队长
3       紫薇一号
5       紫薇三号
被删除的元素:
4       紫薇二号

Process returned 0 (0x0)   execution time : 0.045 s
Press any key to continue.

这里要注意我们删除时传入的是下标,所以传入3实际上是删除了datas[3]也就是4号元素:紫薇二号。

检查长度

同样的,先在SeqyenceList.h头文件中添加操作:

int GetLength(SeqList *seqList);

然后来到SeqyenceList.c文件中,添加操作:

int GetLength(SeqList *seqList){
    if(seqList->length == NULL){
        return 0;
    }
    return seqList->length;
}

判断是否为空

同样的,先在SeqyenceList.h头文件中添加操作:

int IsEmpty(SeqList *seqList);

然后来到SeqyenceList.c文件中,添加操作:

int IsEmpty(SeqList *seqList){
    return GetLength(seqList) == 0 ? true : false;
}

清空线性表

同样的,先在SeqyenceList.h头文件中添加操作:

void ClearList(SeqList *seqList);

然后来到SeqyenceList.c文件中,添加操作:

void ClearList(SeqList *seqList){
    if(seqList == NULL){
        return;
    }
    seqList->length = 0;
}

线性表顺序存储结构的优缺点

优点:

  • 无需因为表示表中的逻辑关系而增加额外的存储空间。
  • 可以快速地存储表中的任意位置的元素。

缺点:

  • 插入和删除要移动大量的元素。
  • 线性表长度变化较大时,难以确定储存空间的容量。
  • 由于难以确定容量,所以有时会造成空间的浪费,还会造成空间的“碎片”。

今天我们整理了线性表中顺序表的知识点,下次我们将整理另一种线性表——链表,我们下次见

你可能感兴趣的:(紫薇星上的数据结构)