顺序表
前言
我们在生活中,常常需要对数据进行存储、处理,而所有的过程总结下来就是“增删查改”四个字,这在我们c语言中也是如此,下面带大家看一看一些简单的对数据进行处理的结构。
一、线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线,但是在物理结构上不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
二、顺序表
2.1概念及结构
顺序表时用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
1.静态顺序表:使用定长数组存储元素
与数组不同的地方就是要求依次存储,比如不可以前面放两个数据,最后放两个数据,中间位置空出
注:typedef用于对类型进行重定义 define通常定义常量,定义宏函数
//顺序表的静态存储
#define N 7
typedef int SLDataType;
typedef struct SeqList
{
SLDataType array[N];//定长数组
size_t size;//有效数据的个数
}SeqList;
因为N的个数不确定,所以静态顺序表存在很大的缺陷,空间开少了不够用,开多了浪费
所以我们一般使用动态顺序表,按需申请,空间不够就扩容
2.动态顺序表:使用动态开辟的数组存储
这是创建一个简单的动态顺序表,那么如何初始化呢?
3.初始化
如果我们想初始化的同时开辟一段空间,我们可以用malloc函数进行
比如说我想要先开辟四个空间,我们可以在LIST头文件中写入宏定义#define INIT_CAPACITY 4(这种方式便于我们以后进行统一修改)
注:perror函数是检查函数,若开辟空间失败会直接返回,避免造成野指针情况。
4.调用
可以看到图片中的调用方法会进行报错,这是因为s未初始化的问题
将s设为全局变量后则不会再报错,因为系统会自动给s初始化
但是我们发现,当调用函数后,s的值并未发生改变,这是因为我们写的是传值调用,在c语言中可以知道,如果想要实参随形参改变而改变,就需要传址调用
5.实现释放功能
6.实现尾插功能
注:realloc能会原地扩容,也可能会异地扩容,如果我们去除ps->a = tmp;这条语句,原地扩容不会报错,但是异地扩容会报错
7.实现尾删功能
8.实现头插功能
9.实现头删功能
10.实现定点插入功能
11.实现定点删除功能
注:申请的空间必须一起释放,不能一部分一部分地释放
而当我们写出定点删除与插入功能时,其实头插头删与尾插尾删功能已经不需要了,因为它们已经包含在这两个功能中了
同时我们可以计算出头插/头删时间复杂度为O(N²),尾插/尾删时间复杂度为O(N) 所以我们在运用过程中要尽量少用头插头删功能
2.2 OJ例题
1.移除元素
题目链接:https://leetcode.cn/problems/remove-element/description/
这道题我们常用的暴力解法时间复杂度为O(N²),但是题目要求我们用时间复杂度为O(N),空间复杂度为O(1)的方法去解决
方法1
当我们不考虑时间时,直接暴力解题
方法2
当我们不考虑空间时,可以额外定义一个数组
假定原数组为nums1,开辟一个数组nums2,初始化i = 0,j = 0
当nums1[i++]与目标数val不同时,赋值给nums2[j++];当nums1[i++]与val相同时,i++,j不变
最后将nums2数组拷贝到nums1数组中即可
方法3
双指针: 这种方法是完美符合题目要求的
2.删除有序数组中的重复项(去重)
题目链接:https://leetcode.cn/problems/remove-duplicates-from-sorted-array/description/
方法1
暴力解题,但不符合题目的时间复杂度与空间复杂度要求
方法2
3.合并两个有序数组(归并)
题目链接:https://leetcode.cn/problems/merge-sorted-array/description/
方法1
暴力解题,全部拷贝到一起,再进行排序,但时间复杂度很高。如果用时间复杂度比较低的快速排序,在一些苛刻情况下(比如面试)是过不了的
方法2
三指针法
注:如果继续从前面开始往后比,比如从0下标开始比较,会存在数据覆盖问题,因此要从后往前比较
总结
我们可以发现顺序表虽然比较好用,但是存在缺陷,比如它的每次扩容一般都是双倍扩容,而假设原本容量为100,我需要205个容量,这扩容使用就会导致浪费许多空间,因此我们引出了链表这一概念。
欲知后事如何,且听下回分解!