目录
1.线性表的定义
2.顺序表
2.1顺序表的定义
2.2 顺序表的应用
2.2.1 顺序表的管理
(1) 顺序表的初始化
(2) 销毁顺序表
(3) 打印顺序表的值
(4)检查顺序表的容量
(5)尾插法
(6) 尾删法
(7) 头插法
(8) 头删法
(9) 测试
2.2.2 顺序表指定位置的插入和删除
(1)在pos位置插入x
(2) 删除pos位置的值
2.2.3 OJ练习题
(1)27. 移除元素 - 力扣(LeetCode)
(2)88. 合并两个有序数组 - 力扣(LeetCode)
1.线性表的定义
线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串。线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
2.顺序表
2.1顺序表的定义
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。如下图所示:
顺序表一般可以分为:
(1)静态顺序表:使用定长数组存储元素。
顺序表采用数组储存,那我们定义的时候就需要定义一个数组,我们采用结构体储存这个结构。静态顺序表的大小是确定的,我们用一个常量N表示。下面给出代码:
//静态顺序表
#define N 1000
typedef int SLDataType;
struct SeqList
{
SLDataType a[N];
int size;//有效数据的个数
};
(2)动态顺序表:使用动态开辟的数组存储。
动态顺序表我们使用一个指针,指针指向数组,在使用的时候使用动态内存函数,动态开辟内存空间。下面给出代码:
// 动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a; //指针指向空间
int size; // 存储有效数据个数
int capacity; // 空间大小
}SL;
2.2 顺序表的应用
实际应用中,由于静态顺序表的大小很难确定,小了不够用,大了浪费空间,所以我们大多时候采用动态顺序表,那么接下来我们就基于动态顺序表进行定义。
2.2.1 顺序表的管理
我们给出一些顺序表的基本操作,增加数据,删除数据,改变数据,查看数据。
// 管理数据 -- 增删查改
void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);
void SLCheckCapacity(SL* ps);
(1) 顺序表的初始化
我们在使用顺序表时候,首先要对顺序表进行初始化。此时我们传入的参数是结构体指针而不是结构体,是为了改变实参。我们为数组a动态开辟了4个SLDataType大小的空间,动态内存开辟不要忘记对开辟的空间进行检查。然后我们初始化此时的空间大小为4,数据使用的空间为0。代码如下:
//初始化顺序表 注意要传指针才能改变实参
void SLInit(SL* ps)
{
//动态开辟内存
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * 4);
if (ps->a == NULL)
{
perror("malloc failed");
exit(-1);//程序报错 直接退出
//return;区分于return返回值
}
ps->size = 0;
ps->capacity = 4;
}
(2) 销毁顺序表
我们是动态开辟的空间,使用完了之后要对顺序表进行销毁。先释放空间,后别忘了对指针置空,防止野指针!代码如下:
//销毁顺序表
void SLDestroy(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
(3) 打印顺序表的值
我们需要一个函数打印出此时顺序表里面的值,size是数组里的有效数据大小,所以我们只需要打印到size。代码如下:
//打印顺序表的值
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
(4)检查顺序表的容量
我们初始化内存大小是4,如果我们此时要存8个数据,就会发生越界。所以我们定义一个函数检查此时顺序表的容量。我们分配的大小为现在大小的两倍,记得检查空间是否成功开辟,此时顺序表的容量就变为原来的两倍。在这里有个细节:我们用的是realloc,它生成的空间有两种不同的方法,忘记的大家可以看一下之前C语言进阶部分内存函数的知识。所以我们一开始把生成的空间用临时变量储存,最后在把临时变量赋给a。防止开辟失败导致内存泄漏。代码如下:
//检查顺序表的容量
void SLCheckCapacity(SL* ps)
{
// 满了要扩容
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, ps->capacity * 2 * (sizeof(SLDataType)));
if (tmp == NULL)
{
perror("realloc failed");
exit(-1);
}
ps->a = tmp;
ps->capacity *= 2;
}
}
(5)尾插法
我们先来了解什么是尾插法,尾插法就是把数据每次都插入顺序表最末尾的位置。那我们在插入前要先检查空间是否够用,然后只需要将数据放入有效数据末尾,增加有效数据长度即可。
// 尾插法
void SLPushBack(SL* ps, SLDataType x)
{
SLCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
(6) 尾删法
尾删法,从尾部一个一个删除数据,但这里我们需要检查有效数据的大小,如果它此时为0,那我们直接执行删除,下一次使用就会导致数组越界。所以我们需要进行一个断言。
//尾删法
void SLPopBack(SL* ps)
{
assert(ps->size > 0);
//ps->a[ps->size - 1] = 0;
ps->size--;
}
(7) 头插法
头插法就是从数组的前面插入值,我们先来看一下实现方法:
我们用这种方法把值往后移动一位,要从后往前覆盖,不能从前往后,防止丢失值。空出开头位给插入数据,大家看懂的话可以尝试自己写一下代码,想一想循环的终止条件。
//头插法:从后往前覆盖值
void SLPushFront(SL* ps, SLDataType x)
{
SLCheckCapacity(ps);
// 挪动数据
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[0] = x;
ps->size++;
}
(8) 头删法
头删法的思想和头插法类似,我们定义一个begin指针,只需要将后面的值往前覆盖,覆盖到最前面的值就好,还是注意循环的终止条件。
//头删法:从后往前覆盖
void SLPopFront(SL* ps)
{
//注意检查size大小
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
(9) 测试
#include<stdio.h>
#include"SeqList.h"
void TestSeqList1()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPushBack(&sl, 5);
SLPushBack(&sl, 6);
SLPushBack(&sl, 6);
SLPushBack(&sl, 0);
SLPushBack(&sl, 0);
SLPrint(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPrint(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
//SLPopBack(&sl);
//SLPopBack(&sl);
/*SLPopBack(&sl);
SLPopBack(&sl);
SLPopBack(&sl);
SLPopBack(&sl);*/
SLPrint(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPrint(&sl);
SLDestroy(&sl);
}
int main()
{
TestSeqList1();
return 0;
}
大家可以根据自己理解想一想测试函数输出的值是多少?看是否充分了解尾插数据插入位置。
void TestSeqList2()
{
SL sl;
SLInit(&sl);
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
SLPushBack(&sl, 5);
SLPrint(&sl);
SLPushFront(&sl, 10);
SLPushFront(&sl, 20);
SLPushFront(&sl, 30);
SLPushFront(&sl, 40);
SLPrint(&sl);
SLPopFront(&sl);
SLPrint(&sl);
}
int main()
{
TestSeqList2();
return 0;
}
大家可以根据自己理解想一想测试函数输出的值是多少?看是否充分了解头插数据插入位置。
2.2.2 顺序表指定位置的插入和删除
(1)在pos位置插入x
在pos位置处插入值x的思路有一些像头插,不过头插是第一个位置,那pos位置插入改变一下循环条件就可以解决,问题是要对pos位置进行限制,pos不能小于0,pos也不能大于size。
// 在pos位置插入x
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
(2) 删除pos位置的值
删除pos位置的值,通过一个begin指针,实现从pos位置后面的值对前面的值进行覆盖。相当于头删的时候,pos值为0。代码实现原理如下:
// 删除pos位置的值
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
2.2.3 OJ练习题
(1)27. 移除元素 - 力扣(LeetCode)
考虑到空间复杂度,这道题我们的思路是用两个指针对数组进行判断。一个src指针,一个dst指针,我们对src指向的值进行判断,若src指向的值不等于val,那我们就把值赋给dst,再把src++和dst++,最后返回数组的长度就是dst。我们来看一下演示步骤:
int removeElement(int* nums, int numsSize, int val) {
int dst=0;
int src=0;
while(src<numsSize)
{
if(nums[src]!=val)
{
nums[dst++]=nums[src++];
}
else
{
src++;
}
}
return dst;
}
(2)88. 合并两个有序数组 - 力扣(LeetCode)
我们先来了解什么是非递减序列:1 2 2 5 6这种序列就是非递减序列,递增序列:1 2 3 5 6,这种就是递增序列。接下来我们思考一下这道题目:要求两个非递减序列合并到大的序列后依旧为非递减序列。我们对两个数组定义两个指针,倒着比较将大的数插入。每个数组最后面的都是最大的数,放在最后就可以保证生成的数组还是非递减数组。代码如下:
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int end1=m-1,end2=n-1,end=m+n-1;
while(end1>=0&&end2>=0)
{
if(nums1[end1]>nums2[end2])
{
nums1[end--]=nums1[end1--];
}
else
{
nums1[end--]=nums2[end2--];
}
}
while(end2>=0)
{
nums1[end--]=nums2[end2--];
}
}