线性表--顺序表 C语言实现

从顺序表开始探索数据结构的魅力

顺序表作为线性表中的一类,可以说是数据结构中最简单的、最基础的一种结构。很多书籍都以此作为开篇,来讲述数据结构的魅力。作为数据结构的开篇,有必要好好学习这一内容,这样才能养成良好的数据结构思维习惯。数据结构从本质上讲,就是定义一种性质,并且在后续的操作中维护这一性质,从而达到特定的目的。

数组杂谈

顺序表实际上可以看作一个“高级”的数组,这是因为数组的性质正好满足顺序表的定义,因而在讨论顺序表前,我们先来看看数组的使用和它的不足。

数组名和指向数组头的指针

数组名和指向数组头的指针很多时候是可以混为一谈的,但严谨的讲,两者是不同的。
我们来看一个代码:

#include <stdio.h>

int main() {
    
    
    int array[] = {
    
    1,2,3,4,5};
    int *p = array;

    printf("sizeof(array) = %d\n",sizeof(array));
    printf("sizeof(p) = %d\n",sizeof(p));

    return 0;
}

在64位机下输出的结果是:

sizeof(array) = 20
sizeof(p)= 8

可以看出,数组名指的是整个数组,其中包含着一个数组占用内存了多少内存,我们可以通过数组名,获取数组中拥有多少个元素。而指向数组头的指针,本质是一个指针变量,它所占的字节数是固定的。
在这里插入图片描述

遍历整个数组

如果只是在数组定义的作用域中,遍历数组是十分简单的,只需要一个数组名即可。

#include <stdio.h>

int main() {
    
    
    int array[] = {
    
    1,2,3,4,5};
	int size = sizeof(array) / sizeof(int);
    for (int i = 0; i < size;i++) {
    
    
        printf("%d ",array[i]);
    }
    printf("\n");

    return 0;
}

但想要跨作用域来实现相同的功能,往往会利用到函数,结合传递参数来实现。但C语言中,我们传递的是一个数组名,但编译器会把它变成一个地址进行传递,从而丢失了数组的长度信息。因此下面的做法明显是错误的:

#include <stdio.h>

void order(int array[]) {
    
    
	int size = sizeof(array) / sizeof(int);
    for (int i = 0; i < size;i++) {
    
    
        printf("%d ",array[i]);
    }
    printf("\n");
}

int main() {
    
    
    int array[] = {
    
    1,2,3,4,5};
    order(array);

    return 0;
}

而为了避免这样的错误,常见的做法,是把数组的长度作为参数,传递给函数。如下:

#include <stdio.h>

void order(int array[],int size) {
    
    
    for (int i = 0; i < size;i++) {
    
    
        printf("%d ",array[i]);
    }
    printf("\n");
}

int main() {
    
    
    int array[] = {
    
    1,2,3,4,5};
    int size = sizeof(array) / sizeof(int);
    order(array,size);

    return 0;
}

从这里我们已经能发现数组的第一个短板了:数组的长度信息会在传参中丢失

数组下标越界访问

数组的下标越界是一种很常见的错误。在C语言规范中,这属于一种未定义行为。各个编译器对其进行的处理各不相同。但有一点是明确的,这样的操作是很危险的。

#include <stdio.h>

#define SIZE 5
int main() {
    
    
    int array[SIZE] = {
    
    1,2,3,4,5};

    for (int i = 0;i < SIZE + 1;i++) {
    
    
        printf("%d ",array[i]);
    }
    printf("\n");

    return 0;
}

上面的代码,可以很容易看出,当遍历执行到最后一次,访问的位置是array[5],这明显是下标越界了,我们并不清楚这一块内存中的数值具体是多少。但这个问题在编译期间是不会被发现的,甚至在运行时也很难察觉,因此数组的第二个不足也呼之欲出:数组下标的越界访问是未定义行为,我们无法对其进行有效的防护

数组中存在无效数据

很多时候,我们为了图省力,一口气开辟一个很大的数组,但实际运行中,只有其中的一部分被使用了(我们假设用的时候一定是从前往后使用的),那么访问没被使用的数组空间同样是一件危险的事。这实际是编程技巧带来的隐患。
打个比方:假设我们要按天记录今年的收入,我们理所当然的会以年作单位,直接拿出一个大本子进行记录,假设今天是4月1号,这时我想查阅5月1号的收入,这时我们能在账单上找到这个日子对应的账单,但我们并没有记录到这一天,所有查阅是可以的,但里面的信息是无效的。

数组存在的问题

通过前面的三个例子。我们已经得出了普通数组存在的缺陷了,主要有两点:

  • 数组的长度信息会在传参中丢失
  • 数组下标的越界访问是未定义行为,我们无法对其进行有效的防护

还有一个是编程技巧带来的隐患:

  • 数组中有些数据是无效的

解决数组存在的不足

既然明确的知道了数组存在的不足,我们就可以对症下药,逐个解决它们。
解决前两个问题的方式前面就提到了,就是增加一个size字段,这样不仅能知晓数组的长度,也能避免下标越界。
而由编程技巧带来的隐患,可以通过增加一个length字段解决。这个字段记录数组前length个数据是有效的,超过length的部分对我们而言是无效数据。
根据上面的分析,我们就可以着手写代码了:

#include <stdio.h>

#define SIZE 100

int main() {
    
    
    int array[SIZE] = {
    
    0};
    int size = sizeof(array) / sizeof(int);   //记录array的长度
    int length = 0;                           //记录array中有效数据的个数


    return 0;
}

通过对sizelength字段的使用,我们可以让数组的操作更加安全,也弥补了数组的先天不足。
至此,实际上我们已经完成了顺序表的结构定义。顺序表其实就是通过数组增加两个字段,达到了比数组更加安全、可靠的目的。

线性表–顺序表

这里有必要理清一下线性表和顺序表之间的关系
线性表是逻辑结构层面的概念,指的是元素间的关系是一对一的
顺序表是物理结构层面的概念,指的是元素存储在一段连续的内存空间中,而数组正好就有这样的性质。

线性表的定义:

线性表(linear list)也被成为有序表(ordered list),它的每一个实例都是元素的一个有序集合。每一个实例的形式为 ( e 0 , e 1 , e 2 , . . . e n − 1 ) (e_0,e_1,e_2,...e_{n-1}) (e0,e1,e2,...en1)
其中n是有穷自然数, e i e_i ei是线性表的元素,i是元素 e i e_i ei索引,n是线性表的长度
可知索引是从0开始的,到n-1结束。

顺序表的结构定义

顺序表实际上就是用一段连续的内存空间实现的线性表,而数组就是不二的选择。其实前面在分析数组的时候,我们就已经把顺序表的结构定义描述出来了,我们将其封装成一个结构体,方便后续操作。

typedef struct SequentialList{
    
    
    int *array;      //数组
    int size;        //记录数组的长度
    int length;      //记录array中有效数据的个数
}SequentialList; 

顺序表的相关操作

顺序表的基本操作有:

  • 创建顺序表
  • 销毁顺序表
  • 判空操作
  • 按一个给定索引查找一个元素
  • 按一个给定索引插入一个元素
  • 按一个给定索引删除一个元素
  • 遍历输出顺序表中的全部元素

等等。接下来我们逐一来实现。
创建和销毁顺序表
因为C语言是没有GC机制的,因而这两个操作往往是成对出现的,我们就一起实现了。
创建顺序表
在创建之前,我们需要清楚的知道,我们需要初始化哪些量,及它们的值各为多少。
不难想到,数组的长度是可以自定义的,我们需要通过传参来控制,并且需要为其开辟相应的内存空间。size字段记录的就是数组长度,也可以确定。而初始化后的顺序表,其有效的元素个数为0,length的值也就确定了。

SequentialList *createSequentialList(int _size) {
    
    
    SequentialList *sl = (SequentialList*)malloc(sizeof(SequentialList));
    sl->array = (int*)malloc(sizeof(int) * _size);
    sl->size = _size;
    sl->length = 0;
    return sl;
}

销毁顺序表
销毁的操作很简单,就是回收堆上的内存,防止内存泄漏。

void destroySequentialList(SequentialList *sl) {
    
    
    if (sl == NULL) return;

    free(sl->array);   //回收数组所占内存
    free(sl);          //回收顺序表对象的内存

    return;
}

判空操作
判空操作实际上是一个辅助操作,我们只要知道现在的有效数据个数是不是0,就可以判断了。

int is_empty(SequentialList *sl) {
    
    
    if (sl == NULL) return -1;
    return sl->length == 0;
}

按一个给定索引查找一个元素
之间根据下标返回元素即可,但需要注意数据索引的有效范围应当是0到length - 1的。

int getElement(SequentialList *sl,int index) {
    
    
    if (sl == NULL) return -1;
    if (index < 0 || index > sl->length) return -1;   

    return sl->array[index];
}

按一个给定索引插入一个元素
同样需要注意索引的合法取值,应为0到length(因为是插入,插入到末尾是合理的)。同时,插入操作,需要让索引后面的所有值,都依次向后一位,这样才有空的位置可供插入。而向后一位的顺序应当是最后一位开始移动的。
在这里插入图片描述

int insert(SequentialList *sl,int index,int element) {
    
    
    if (sl == NULL) return 0;
    if (index < 0 || index > sl->length) return 0;
    if (sl->length == sl->size) return 0;

    for (int i = sl->length;i > index; i--) {
    
    
        sl->array[i] = sl->array[i - 1];
    }
    sl->array[index] = element;
    sl->length++;

    return 1;
}

按一个给定索引删除一个元素
技巧和插入差不多,不同的是,这次元素的移动方向是从索引的后一个元素开始,统一往前移动一位。
在这里插入图片描述

int erase(SequentialList *sl,int index) {
    
    
    if (sl == NULL) return 0;
    if (index < 0 || index > sl->length) return 0;

    for (int i = index + 1; i < sl->length; i++) {
    
    
        sl->array[i - 1] = sl->array[i]; 
    }
    sl->length--;

    return 1;
}

遍历输出顺序表中的全部元素

void display(SequentialList *sl) {
    
    
    if (sl == NULL) return;
    if (is_empty(sl)) return;

    for(int i = 0; i < sl->length; i++) {
    
    
        printf("%d ",sl->array[i]);
    }
    printf("\n");

    return;
}

顺序表的整体实现

#include <stdio.h>
#include <stdlib.h>

//顺序表结构定义
typedef struct SequentialList{
    
    
    int *array;      //数组
    int size;        //记录数组的长度
    int length;      //记录array中有效数据的个数
}SequentialList; 

//创建顺序表   
SequentialList *createSequentialList(int _size) {
    
    
    SequentialList *sl = (SequentialList*)malloc(sizeof(SequentialList));
    sl->array = (int*)malloc(sizeof(int) * _size);
    sl->size = _size;
    sl->length = 0;
    return sl;
}

//销毁顺序表
void destroySequentialList(SequentialList *sl) {
    
    
    if (sl == NULL) return;

    free(sl->array);   //回收数组所占内存
    free(sl);          //回收顺序表对象的内存

    return;
}

//判空操作
int is_empty(SequentialList *sl) {
    
    
    if (sl == NULL) return -1;
    return sl->length == 0;
}

//按一个给定索引查找一个元素
int getElement(SequentialList *sl,int index) {
    
    
    if (sl == NULL) return -1;
    if (index < 0 || index > sl->length) return -1;   

    return sl->array[index];
}

//按一个给定索引插入一个元素
int insert(SequentialList *sl,int index,int element) {
    
    
    if (sl == NULL) return 0;
    if (index < 0 || index > sl->length) return 0;
    if (sl->length == sl->size) return 0;

    for (int i = sl->length;i > index; i--) {
    
    
        sl->array[i] = sl->array[i - 1];
    }
    sl->array[index] = element;
    sl->length++;

    return 1;
}

//按一个给定索引删除一个元素
int erase(SequentialList *sl,int index) {
    
    
    if (sl == NULL) return 0;
    if (index < 0 || index > sl->length) return 0;

    for (int i = index + 1; i < sl->length; i++) {
    
    
        sl->array[i - 1] = sl->array[i]; 
    }
    sl->length--;

    return 1;
}

//遍历输出顺序表中的全部元素
void display(SequentialList *sl) {
    
    
    if (sl == NULL) return;
    if (is_empty(sl)) return;

    for(int i = 0; i < sl->length; i++) {
    
    
        printf("%d ",sl->array[i]);
    }
    printf("\n");

    return;
}

int main() {
    
    
    SequentialList *sl = createSequentialList(100);           //创建顺序表
    
    for (int i = 0; i < 10; i++) {
    
      
        if (!insert(sl,i,i))                                  //按一个给定索引插入一个元素
            return -1;                                        //插入失败
    }
    display(sl);                                              //遍历输出顺序表中的全部元素

    for (int i = 0;i < 5; i++) {
    
    
        if (!erase(sl,0))                                     //按一个给定索引删除一个元素
            return -1;                                        //删除失败
    }
    display(sl);                                              //遍历输出顺序表中的全部元素

    for (int i = 0; i < sl->length; i++) {
    
    
        printf("索引%d对应的数据为:%d\n",i,getElement(sl,i)); //按一个给定索引查找一个元素 
    }

    destroySequentialList(sl);                                //销毁顺序表
    return 0;
}

顺序表的优缺点

顺序表因为是由连续内存空间实现的,它的优缺点很明显
优点:查找操作快,之间下标索引,时间复杂度O(1)
缺点:插入、删除操作慢,需要遍历整个数组,时间复杂度O(n)

后记

C语言的数组存在的缺陷,在很多语言中其实已经被消除了。Java、C#等语言的数组本身就带有长度等信息。但这不影响我们的顺序表学习,数据结构归根结底学的是一种思维逻辑,一旦理解了某个结构存在的意义,以及其现实的用途,我们才能把它用到合适的地方中去,发挥它真正的作用。

猜你喜欢

转载自blog.csdn.net/weixin_43003108/article/details/113732423