线性表
线性表是n个具有相同特性的数据元素的有序序列。线性表在逻辑上是连续的,在物理上不一定连续。所以线性表的逻辑结构一定是线性的,但物理结构不一定是线性的。
补充概念:
物理结构就是实际在内存中的结构,就比如像数组在内存里边是一段连续的空间,那么它的物理结构就是线性的。
逻辑结构是我们人为想象出来的结构。
顺序表
顺序表就是用一段物理地址连续的存储单元依次存储数据的线性结构,一般采用数组去存储。它的逻辑结构和物理结构都是线性的。
既然顺序表是用数组去实现的,那数组跟顺序表的区别是什么呢?
我们可以理解为数组是顺序表的一部分,顺序表是数组的plus版,它封装了用于增删查改的函数,可以供我们直接使用。顺序表的底层结构是数组,顺序表是由数组去实现的。
基于以上的说明,我们先给顺序表分个类,顺序表分为静态顺序表 和动态顺序表。
静态顺序表:
定义如下
cpp
#define N 7
typedef int SLDateType;
typedef struct SeqList
{
SLDateType arr[N];
int size;
}SL;
我们用#define定义了一个常量,其值作为静态顺序表的总容量,由于顺序表不可能一开始就全部装满数据,所以通过定义size来存储当前顺序表里有效的元素个数,另外,我们给存放在顺序表里的元素类型取了一个别名SLDateType,目的是方便我们后续一键修改,因为数组里还可能存放譬如char,long long之类的数据类型。至于给结构体取别名的作用是为了在后续定义顺序表的时候简化一下我们的代码。
静态顺序表的缺点就是空间给小了不够用,给多了又造成空间的浪费。
动态顺序表:
动态顺序表相比较于静态的顺序表来说就显得非常的灵活了。当空间不够的时候可以直接通过扩容来解决,因此,如果在顺序表的容量无法确定的情况下,我们就使用动态顺序表就可以了。
顺序表的模拟实现(动态)
**前言:**一般在实际的工程里边,我们会创建3个文件,.h头文件,test.c测试文件和.c实现文件。其中头文件是专门用来放定义和声明的,实现文件是放函数的具体实现的,而test.c文件是用来测试我们自己写的代码是否存在bug的。(如下)



顺序表的定义与接口
定义:
动态顺序表的定义我们用到的是动态开辟的数组。跟静态的顺序表同理,需要size变量来标记有效的元素个数,与之不同的是,我们还需要capacity变量来标记空间容量,以便空间不够时候的扩容。
cpp
//定义
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* arr;
int size;
int capacity;
}SL;
初始化:SLInit
顺序表的初始化很简单,只需要将arr的值初始化为NULL,size和capacity的值初始化为0即可。值得注意的是,当我们给SLInit函数传参的时候,传的是地址,而不是值,因为传值调用,形参是实参的值的拷贝,相当于将我们要初始化的那个顺序表拷贝了一份再初始化,但我们就是想要给该顺序表初始化呀,传值过去就导致了编译报错。
传值调用初始化的效果类似如下:
定义:
int a;Init(int b);
调用:
Init(a);//但a自己都没初始化,怎么还传它的值用来给b初始化呢?
cpp
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
尾插:SLPushback 时间复杂度 O(1)
但凡涉及到插入操作,都要考虑空间是否够的问题。
1.空间不够的情况。
需要搞明白下边的两个问题。
如何判断空间是否满了?
只需要判断size==capacity是否成立就可以了。
如何增容?
如果每次只增加一个空间,那就会出现频繁增容的情况,我们知道realloc的工作原理,如果在原来申请的空间之后还有额外的空间,那它就会直接接在后边增容,如果在原来申请的空间之后没有足够的空间了,那它就会在堆区重新找一块新空间,开辟成我们想要的大小,然后将原数据拷贝到新找到的这块空间里边并且释放掉原来的空间。那如果频繁的增容,就会导致程序执行效率低下的情况。
如果一下子扩容的很大,就会导致空间的浪费。
因此,通常我们是采取2倍增容的方式。这样最科学。
2.空间足够的情况。
当空间足够的时候,只需要在arr数组size下标的位置插入数据就可以了,插入完之后size++即可。
cpp
//尾插
void SLPushback(SL* ps, SLDateType x)
{
//断言一下,如果ps传过来的是一个NULL地址,就不用再进行头插操作了。
assert(ps);
//空间不够--扩容
if (ps->capacity == ps->size)
{
//newCapacity就是我们要扩容的大小
//当capacity为0的时候,我们给数组扩容一个初始值4(代表顺序表里边为4个SLDateType的空间大小)
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDateType* tmp = (SLDateType*)realloc(ps->arr, newCapacity * sizeof(SLDateType));
if (tmp == NULL)
{
perror("realloc");
return;
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
//空间够
ps->arr[ps->size++] = x;
}
头插:SLPushfront 时间复杂度 O(N)
举个例子:
对于顺序表 1 2 3 4 5 来说,要往1的前边插入一个数据,需要将1~5统一先向后移动一个单位,将下标为0的位置空出给待插入的元素,注意移动的时候是从后往前一个个元素往后移动一个位置。
cpp
//头插
void SLPushfront(SL* ps, SLDateType x)
{
//断言一下,如果ps传过来的是一个NULL地址,就不用再进行头插操作了。
assert(ps);
//空间不够--扩容
if (ps->capacity == ps->size)
{
//newCapacity就是我们要扩容的大小
//当capacity为0的时候,我们给数组扩容一个初始值4(代表顺序表里边为4个SLDateType的空间大小)
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDateType* tmp = (SLDateType*)realloc(ps->arr, newCapacity * sizeof(SLDateType));
if (tmp == NULL)
{
perror("realloc");
return;
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
for (int i = ps->size - 1; i >= 0; i--)
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[0] = x;
++ps->size;
}
尾删:SLPopback 时间复杂度 O(1)
所有的删除操作都要考虑到删空的情况,需要判空。
顺序表的尾删,只需要将size--就可以了。
cpp
//尾删
void SLPopback(SL* ps)
{
assert(ps && ps->size);
ps->size--;
}
头删:SLPopfront 时间复杂度 O(N)
需要将下标为1到size - 1之间的元素依次往前移动一位即可完成头删的操作。也就是直接将我们要删除的下标为0的元素给覆盖掉就行了。最后别忘了size--。
cpp
//头删
void SLPopfront(SL* ps)
{
assert(ps && ps->size);
for (int i = 1; i < ps->size; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
ps->size--;
}
在指定位置之前插入数据:SLInsert 时间复杂度 O(N)
跟头插很像,需要将下标为pos~ps->size - 1之间的所有元素依次向后移动一位,空出的pos位置留给要插入的元素。
cpp
//在指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDateType x)
{
//pos为想要插入的位置,要assert一下,不能越界
assert(ps);
assert(pos >= 0 && pos <= ps->size);
//空间不够--扩容
if (ps->capacity == ps->size)
{
//newCapacity就是我们要扩容的大小
//当capacity为0的时候,我们给数组扩容一个初始值4(代表顺序表里边为4个SLDateType的空间大小)
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDateType* tmp = (SLDateType*)realloc(ps->arr, newCapacity * sizeof(SLDateType));
if (tmp == NULL)
{
perror("realloc");
return;
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
for (int i = ps->size - 1; i >= pos; i--)
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[pos] = x;
ps->size++;
}
删除pos位置的数据:SLErase 时间复杂度 O(N)
跟头删很像,只需要将下标为(pos + 1) ~ (ps->size - 1)的数据依次向前移动一位就可以了。直接取代掉pos位置那个我们要删除的值。
cpp
//删除pos位置的数据
void SLPop(SL* ps, int pos)
{
assert(ps && ps->size);
assert(pos >= 0 && pos < ps->size);
for (int i = pos + 1; i < ps->size; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
ps->size--;
}
销毁:SLDestroy
释放掉之前开辟的所有的空间,然后将size和capacity置为0即可。
cpp
//销毁
void SLDestroy(SL* ps)
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
下边给出完整的.h定义声明文件和.c实现文件里的代码:
SeqList.h
cpp
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
//定义
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* arr;
int size;
int capacity;
}SL;
//初始化
void SLInit(SL* ps);
//尾插
void SLPushback(SL* ps, SLDateType x);
//头插
void SLPushfront(SL* ps, SLDateType x);
//尾删
void SLPopback(SL* ps);
//头删
void SLPopfront(SL* ps);
//在指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDateType x);
//删除pos位置的数据
void SLPop(SL* ps, int pos);
//销毁
void SLDestroy(SL* ps);
SeqList.c
cpp
#include"SeqList.h"
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
//由于相同的扩容代码要多次写---因此直接封装成一个函数
void SLCheckCapacity(SL* ps)
{
//空间不够--扩容
if (ps->capacity == ps->size)
{
//newCapacity就是我们要扩容的大小
//当capacity为0的时候,我们给数组扩容一个初始值4(代表顺序表里边为4个SLDateType的空间大小)
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDateType* tmp = (SLDateType*)realloc(ps->arr, newCapacity * sizeof(SLDateType));
if (tmp == NULL)
{
perror("realloc");
return;
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
//尾插
void SLPushback(SL* ps, SLDateType x)
{
assert(ps);
SLCheckCapacity(ps);
//空间够
ps->arr[ps->size++] = x;
}
//头插
void SLPushfront(SL* ps, SLDateType x)
{
//断言一下,如果ps传过来的是一个NULL地址,就不用再进行头插操作了。
assert(ps);
SLCheckCapacity(ps);
for (int i = ps->size - 1; i >= 0; i--)
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[0] = x;
++ps->size;
}
//尾删
void SLPopback(SL* ps)
{
assert(ps && ps->size);
ps->size--;
}
//头删
void SLPopfront(SL* ps)
{
assert(ps && ps->size);
for (int i = 1; i < ps->size; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
ps->size--;
}
//在指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDateType x)
{
//pos为想要插入的位置,要assert一下,不能越界
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
for (int i = ps->size - 1; i >= pos; i--)
{
ps->arr[i + 1] = ps->arr[i];
}
ps->arr[pos] = x;
ps->size++;
}
//删除pos位置的数据
void SLPop(SL* ps, int pos)
{
assert(ps && ps->size);
assert(pos >= 0 && pos < ps->size);
for (int i = pos + 1; i < ps->size; i++)
{
ps->arr[i - 1] = ps->arr[i];
}
ps->size--;
}
//销毁
void SLDestroy(SL* ps)
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
与顺序表相关的几道算法题
1.移除元素:https://leetcode.cn/problems/remove-element
解题思路:
思路1:创建新数组
我们可以创建一个跟题给数组nums一样的数组,便利一遍nums数组,将nums数组里边不是val的值依次赋值到新数组里边,最后再将新数组拷贝给给nums数组。时间复杂度为O(N)
思路2:双指针
我们定义两个指针src和dst,src负责在前边探路寻找nums数组里边不是val的值,dst指向的是最后一个不是val的值。相当于给数组分块,分成两个部分,前一个部分的值都不是val,后一个部分的值都是val。以dst为块的分界点。
当nums[src] != val,就让src指向的数据给给dst就可以了,dst++,src++
当nums[src] == val,就直接src++就可以了。
代码实现:
cpp
//思路一
int removeElement(int* nums, int numsSize, int val) {
//考虑nums数组为空的情况
if(numsSize == 0)
{
return 0;
}
int num[numsSize];
int j = 0;//j用来标记nums中不等于val的值
for(int i = 0;i < numsSize;i++)
{
if(nums[i] != val)
{
num[j++] = nums[i];
}
}
//num里边只有j个不是val的数据,拷贝到nums数组即可
for(int i = 0;i < j;i++)
{
nums[i] = num[i];
}
return j;
}
//思路二
int removeElement(int* nums, int numsSize, int val) {
int src = 0,dst = 0;
while(src < numsSize)
{
if(nums[src] != val)
{
nums[dst] = nums[src]
dst++;
}
src++;
}
return dst;
}
2.删除有序数组中的重复项:https://leetcode.cn/problems/remove-duplicates-from-sorted-array
解题思路:
利用双指针,与上题不同的是,src初始化为1,dst初始化为0,因为题目说要找出唯一只出现过一次的元素数量,那下标为0的那个元素肯定是第一次出现,就不用管它。
当nums[src] == nums[dst],src++,此时src指向的是重复的元素,直接跳过即可。
当nums[src] != nums[dst],nums[dst + 1] = nums[src] ,src++ ,dst++,注意这里的赋值是给给dst + 1位置的元素,因为dst位置的元素是我们要的不重复的元素。
代码实现:
cpp
int removeDuplicates(int* nums, int numsSize) {
int src = 1,dst = 0;
while(src < numsSize)
{
if(nums[src] != nums[dst])
{
nums[dst + 1] = nums[src];
dst++;
}
src++;
}
return dst + 1;
}
3.合并两个有序数组:https://leetcode.cn/problems/merge-sorted-array
解题思路:
思路1:创建一个m + n大小的数组
先创建一个m + n大小的num数组,同时便利nums1和nums2数组,比较它们的元素大小,小的先给给到num数组里边,然后看nums1和nums2哪个数组有元素剩余就依次便利给给num就可以了,最后将num数组里边的元素全部给给到nums1数组。
思路2:指针法
定义三个指针i,j,k,分别指向nums1第m个数据的位置,nums1最后一个下标的位置和nums2最后一个下标的位置。比较nums1[i]和nums2[k]的大小,哪个大,哪个放到nums[j]的位置,下标减减,最后如果nums1先便利完,就直接将nums2的元素依次从后往前给给nums1,那如果是nums2先便利完,就不用动了,因为最后题干要求的就是数据存放在nums1数组里边。
注意为什么不从前往后便利?因为从前往后便利比较大小赋值给给之后会导致数据被覆盖,无法实现我们想要的效果。
代码实现:
cpp
//思路一:
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
//i和j分别用来便利nums1数组和nums2数组
//k用来标记num数组的下标
int i = 0,j = 0,k = 0;
int num[m + n];
while(i < m && j < n)
{
if(nums1[i] <= nums2[j])
{
num[k] = nums1[i];
i++;
}
else
{
num[k] = nums2[j];
j++;
}
k++;
}
//判断哪个数组里边还有元素,就直接依次给给num数组即可
while(i < m) num[k] = nums1[i],k++,i++;
while(j < n) num[k] = nums2[j],k++,j++;
//num给给nums1
for(int i = 0;i < m + n;i++)
{
nums1[i] = num[i];
}
}
//思路二:
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int i = m - 1,j = m + n - 1,k = n - 1;
while(i >= 0 && k >= 0)
{
if(nums1[i] >= nums2[k])
{
nums1[j] = nums1[i];
i--;
j--;
}
else
{
nums1[j] = nums2[k];
k--;
j--;
}
}
//如果nums2里边还有数据,就全部给给nums1
while(k >= 0)
{
nums1[j] = nums2[k];
k--;
j--;
}
}