1.前言
数据结构为了应对不同的场景将数据在内存中储存起来,方便展示和搜索,下面就来看顺序表。
2.线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串... 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
3.顺序表
3.1顺序表的概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,也就是从开始到结尾是连续存储的。一般情况下采用数组存储。在数组上完成数据的增删查改。也就是写一个结构,通过管理结构对数据进行管理。顺序表一般可以分为:1. 静态顺序表:使用定长数组存储元素。2. 动态顺序表:使用动态开辟的数组存储。
3.1.1静态顺序表的结构
静态顺序表是静态的,也就是它存储数据的个数是固定的,不能改变。因此需要定义一个大小固定的数组,还需要一个变量来记录数组中实际存储有效数据的个数,以方便后期增删查改的实现。
struct SeqList
{
int a[10];
int size;
};
但实际中我们不知道要存储的数据的数据类型是什么以及万一后期想把a[10]改成a[30],后面好多设计a[10]的地方都要更着改,不太方便,因此做一下调整。
typedef int SLDataType;
#define N 10
struct SeqList
{
SLDataType a[N];
int size;
};
这样一个静态顺序表就定义好了,静态顺序表只适用于确定知道需要存多少数据的场景,因为数组写死的缘故,数据的空间开小了可能不够用,开大了浪费空间。因此实际中很少用静态的顺序表,这里也不过多往后实现接口。
3.1.2动态顺序表的结构
动态顺序表是动态的,也就是它存储数据的个数不是固定的,可以根据实际情况改变。因此需要定义一个动态的数组,这里就想到在堆上开辟一个动态数据,用指针指向这一块空间。同时有个变量来记录此时数组的空间容量,还有一个变量记录存在数组中的有效数据的个数。除了前面提到为了后续的方便操作,再加上后期实现接口传结构体类型参数比较长,因此这样定义。
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size;
int capacity;
}SL;
这样就可以按需索取了。
3.2接口实现
在日常生活中,通过数据结构对数据的管理我们经常用到增、删、查、改。但顺序表使用前是要进行初始化的,最后用完后也要销毁它。
3.2.1初始化
可以先给动态数组开一小部分空间,这里我选择开4个大小的空间,初始化这里想开几个都可以,但为了后续万一修改要一连串的改,则用define定义一个符号。
void SLInit(SL s)
{
SLDataType* tmp = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
if (tmp == NULL)
{
perror("malloc");
return;
}
s.a = tmp;
s.size = 0;
s.capacity = INIT_CAPACITY;
}
这里我们测试一下看看有没有什么问题。
SL s; //因此用全局变量
void TestSeqList()
{
//SL s; 局部变量没有初始化编译器会报错
SLInit(s);
}
int main()
{
TestSeqList();
return 0;
}
通过监视窗口我们看到,函数进去时确实初始化了,但出来时并没有初始化,这是因为函数形参是实参的一份临时拷贝,形参的改变并不会影响实参,因此重新改为这样。其次检查可能为空的地方。
void SLInit(SL* s)
{
assert(s);
SLDataType* tmp = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
if (tmp == NULL)
{
perror("malloc");
return;
}
s->a = tmp;
s->size = 0;
s->capacity = INIT_CAPACITY;
}
再次测试:
void TestSeqList()
{
SL s; //传地址定义局部变量也可以
SLInit(&s);
}
int main()
{
TestSeqList();
return 0;
}
函数结束后也初始化成功。
3.2.2销毁
销毁时把堆上开辟的空间释放了,再将大小和容量置为0即可。
void SLDestroy(SL* s)
{
assert(s);
free(s->a);
s->a = NULL;
s->capacity = s->size = 0;
}
3.2.3尾插
在顺序表的尾部插入一个数据,size记录顺序表中有效数据的个数,如果作为下标,它指向的是最后一个数据的下一个位置,因此尾插时直接在该位置放值就可以了。完成后有效数据多一个。
void SLPushBack(SL* s, SLDataType x)
{
assert(s);
s->a[s->size] = x;
s->size++;
}
那这样有没有问题呢?当不断的插入数据,数据满了之后就不能继续插入了,此时需要进行扩容。扩容一般扩容2倍。扩容后不要忘记修改capacity。
void SLPushBack(SL* s, SLDataType x)
{
assert(s)
if (s->capacity == s->size)
{
SLDataType* tmp = (SLDataType*)realloc(s->a, s->capacity * 2);
if (tmp == NULL)
{
perror("realloc");
return;
}
s->a = tmp;
s->capacity *= 2;
}
s->a[s->size] = x;
s->size++;
}
这样写有没有问题呢?我们插入几个数据来试一试。为了方便观察写一个函数来打印插入的值。
void SLPrint(SL* s)
{
assert(s);
for (int i = 0; i < s->size; i++)
{
printf("%d ", s->a[i]);
}
printf("\n");
}
这里发现结果与我们设想的并不一样。这里涉及到了越界,初始化的时候给数组开辟了4个int类型的空间,大小是16个字节。扩容时本应该扩容到32个字节,这里却扩容到8个字节。因此做以下调整。
void SLPushBack(SL* s, SLDataType x)
{
assert(s);
if (s->capacity == s->size)
{
SLDataType* tmp = (SLDataType*)realloc(s->a, sizeof(SLDataType) * s->capacity * 2);
if (tmp == NULL)
{
perror("realloc");
return;
}
s->a = tmp;
s->capacity *= 2;
}
s->a[s->size] = x;
s->size++;
}
这样就不会有上述问题。
3.2.4尾删
因为size记录的是有效数据的个数,同时作为下标的界限,因此尾删时减少size的个数即可。有时候会把尾删位置的值置为0,其实不用这样,因为可能这一项本来就是0。这里多余空间不用释放,因为free释放是从头开始释放。
void SLPopBack(SL* s)
{
assert(s);
s->size--;
}
这里有什么问题呢?问题就是不能一直减下去。
void SLPopBack(SL* s)
{
assert(s);
assert(s->size > 0);
s->size--;
}
这样可以防止越界。
3.2.4头插
定义一个变量指向最后一个元素,一个一个向后挪动,当头部布置的元素挪动到下一个位置的时候停止,然后在头部插入所要插入的元素。不要忘记有效数据的个数增加。
void SLPushFront(SL* s, SLDataType x)
{
assert(s);
int end = s->size - 1;
while (end >= 0)
{
s->a[end + 1] = s->a[end];
end--;
}
s->a[0] = x;
s->size++;
}
这里的问题还是设计代码写满需要扩容的问题,因此我们将扩容写成一个函数,以后扩容掉用函数即可。
void SLCheckCapacity(SL* s)
{
if (s->capacity == s->size)
{
SLDataType* tmp = (SLDataType*)realloc(s->a, sizeof(SLDataType) * s->capacity * 2);
if (tmp == NULL)
{
perror("realloc");
return;
}
s->a = tmp;
s->capacity *= 2;
}
}
void SLPushFront(SL* s, SLDataType x)
{
assert(s);
SLCheckCapacity(s);
int end = s->size - 1;
while (end >= 0)
{
s->a[end + 1] = s->a[end];
end--;
}
s->a[0] = x;
s->size++;
}
3.2.5头删
定义一个变量指向第二个位置,将第二个位置的元素依次挪动到第一个位置,直到将size-1位置的元素挪走即可。挪走都不要忘记size-1;
void SLPopFront(SL* s)
{
assert(s);
int begin = 1;
while (begin < s->size)
{
s->a[begin - 1] = s->a[begin];
begin++;
}
s->size--;
}
这里问题是没有元素的时候继续减会让size减到-1,当再用size时候会越界。
void SLPopFront(SL* s)
{
assert(s);
assert(s->size > 0);
int begin = 1;
while (begin < s->size)
{
s->a[begin - 1] = s->a[begin];
begin++;
}
s->size--;
}
对比分析:头插N个元素,时间复杂度:O(N^2)。(第二次插挪动一次,第三次挪动二次……)。尾插N个元素,时间复杂度:O(N)。(直接尾部放)。头删N个元素,时间复杂度:O(N^2)。(删一次挪动n-1次……)。尾删N个元素,时间复杂度:O(N)。因此持续的头插和头删效率并不是很高。
3.2.6某个位置插入
有时候不想把数组放在头部和尾部,想在指定的位置放数据,这时候就需要这样一个接口。定义一个pos来指定我们想要插入数据的位置。pos位置不能随意指定,可能造成越界,因此pos的位置只能在[开始,结尾下一个]之间,闭区间相当于头插和尾插。还要定义一个变量指向最后一个元素,从最后一个元素开始依次向后挪动,值到挪动完pos位置停止,这样就可以在pos位置插入元素了。完成后不要忘记让有效数据个数增加,还要考虑扩容的问题。
void SLInsert(SL* s, int pos, SLDataType x)
{
assert(s);
assert(pos >= 0 && pos <= s->size);
SLCheckCapacity(s);
int end = s->size - 1;
while (end >= pos)
{
s->a[end + 1] = s->a[end];
end--;
}
s->a[pos] = x;
s->size++;
}
有了这个接口,头插和尾插的实现就可以复用该函数。
void SLPushFront(SL* s, SLDataType x)
{
//assert(s);
//SLCheckCapacity(s);
//int end = s->size - 1;
//while (end >= 0)
//{
// s->a[end + 1] = s->a[end];
// end--;
//}
//s->a[0] = x;
//s->size++;
SLInsert(s, 0, x);
}
void SLPushBack(SL* s, SLDataType x)
{
/*if (s->capacity == s->size)
{
SLDataType* tmp = (SLDataType*)realloc(s->a, sizeof(SLDataType) * s->capacity * 2);
if (tmp == NULL)
{
perror("realloc");
return;
}
s->a = tmp;
s->capacity *= 2;
}*/
/*assert(s);
SLCheckCapacity(s);
s->a[s->size] = x;
s->size++;*/
SLInsert(s, s->size, x);
}
3.2.7某个位置删除
有时候不想在头部和尾部删除数据,想在指定的位置删除数据,这时候就需要这样一个接口。定义一个pos来指定我们想删除数据的位置。pos位置不能随意指定,可能造成越界,因此pos的位置只能在[开始,结尾]之间,闭区间相当于头删和尾删。还要定义一个变量指向pos的下一个位置,让变量指向的位置向前依次覆盖,直到最后一个元素向前覆盖完成。不要忘了有效数据个数减少和数据没有了继续减。这里因为断言的存在不用在单独判断。
void SLErase(SL* s, int pos)
{
assert(s);
assert(pos >= 0 && pos < s->size);
int begin = pos + 1;
while (begin < s->size)
{
s->a[begin - 1] = s->a[begin];
begin++;
}
s->size--;
}
有了这个接口,尾删和头删的实现就可以复用该函数。
void SLPopBack(SL* s)
{
/*assert(s);
assert(s->size > 0);
s->size--;*/
SLErase(s, s->size - 1);
}
void SLPopFront(SL* s)
{
/*assert(s);
assert(s->size > 0);
int begin = 1;
while (begin < s->size)
{
s->a[begin - 1] = s->a[begin];
begin++;
}
s->size--;*/
SLErase(s, 0);
}
3.2.7查找
查找想要的元素,如果找到了就返回它的下标。这里就遍历一遍查找即可。
int SLFind(SL* s, SLDataType x)
{
assert(s);
for (int i = 0; i < s->size; i++)
{
if (s->a[i] == x)
{
return i;
}
}
return -1;
}
3.3顺序表代码
//.h文件
//
//typedef int SLDataType;
//#define N 10
//
//struct SeqList
//{
// SLDataType a[N];
// int size;
//};
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
#define INIT_CAPACITY 4
typedef struct SeqList
{
SLDataType* a;
int size;
int capacity;
}SL;
void SLInit(SL* s);
void SLDestroy(SL* s);
void SLPrint(SL* s);
void SLCheckCapacity(SL* s);
void SLPushBack(SL* s, SLDataType x);
void SLPopBack(SL* s);
void SLPushFront(SL* s, SLDataType x);
void SLPopFront(SL* s);
void SLInsert(SL* s, int pos, SLDataType x);
void SLErase(SL* s, int pos);
int SLFind(SL* s, SLDataType x);
//.c文件
#include "SeqList.h"
void SLInit(SL* s)
{
assert(s);
SLDataType* tmp = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
if (tmp == NULL)
{
perror("malloc");
return;
}
s->a = tmp;
s->size = 0;
s->capacity = INIT_CAPACITY;
}
void SLDestroy(SL* s)
{
assert(s);
free(s->a);
s->a = NULL;
s->capacity = s->size = 0;
}
void SLPrint(SL* s)
{
assert(s);
for (int i = 0; i < s->size; i++)
{
printf("%d ", s->a[i]);
}
printf("\n");
}
void SLCheckCapacity(SL* s)
{
assert(s);
if (s->capacity == s->size)
{
SLDataType* tmp = (SLDataType*)realloc(s->a, sizeof(SLDataType) * s->capacity * 2);
if (tmp == NULL)
{
perror("realloc");
return;
}
s->a = tmp;
s->capacity *= 2;
}
}
void SLPushBack(SL* s, SLDataType x)
{
/*if (s->capacity == s->size)
{
SLDataType* tmp = (SLDataType*)realloc(s->a, sizeof(SLDataType) * s->capacity * 2);
if (tmp == NULL)
{
perror("realloc");
return;
}
s->a = tmp;
s->capacity *= 2;
}*/
/*assert(s);
SLCheckCapacity(s);
s->a[s->size] = x;
s->size++;*/
SLInsert(s, s->size, x);
}
void SLPopBack(SL* s)
{
/*assert(s);
assert(s->size > 0);
s->size--;*/
SLErase(s, s->size - 1);
}
void SLPushFront(SL* s, SLDataType x)
{
//assert(s);
//SLCheckCapacity(s);
//int end = s->size - 1;
//while (end >= 0)
//{
// s->a[end + 1] = s->a[end];
// end--;
//}
//s->a[0] = x;
//s->size++;
SLInsert(s, 0, x);
}
void SLPopFront(SL* s)
{
/*assert(s);
assert(s->size > 0);
int begin = 1;
while (begin < s->size)
{
s->a[begin - 1] = s->a[begin];
begin++;
}
s->size--;*/
SLErase(s, 0);
}
void SLInsert(SL* s, int pos, SLDataType x)
{
assert(s);
assert(pos >= 0 && pos <= s->size);
SLCheckCapacity(s);
int end = s->size - 1;
while (end >= pos)
{
s->a[end + 1] = s->a[end];
end--;
}
s->a[pos] = x;
s->size++;
}
void SLErase(SL* s, int pos)
{
assert(s);
assert(pos >= 0 && pos < s->size);
int begin = pos + 1;
while (begin < s->size)
{
s->a[begin - 1] = s->a[begin];
begin++;
}
s->size--;
}
int SLFind(SL* s, SLDataType x)
{
assert(s);
for (int i = 0; i < s->size; i++)
{
if (s->a[i] == x)
{
return i;
}
}
return -1;
}
4.例题
4.1
https://leetcode-cn.com/problems/remove-element/https://leetcode-cn.com/problems/remove-element/ 思路一:遍历数组,找到val,就将val后面的值向前挪动。缺点是val如果很多则时间复杂度:O(N^2)。
思路二:定义src指向原数组,dest指向新开辟的数组,遍历数组,不是val的值放到dest中,是val的值不用放,遍历完程序结束。缺点是空间复杂度是O(N)。
思路三:结合思路二,能不能在当前数组中完成上面的思路,这样就不用开辟新的空间。定义src的dest指向数组首元素,看src有没有指向val,如果没有就让src位置的值赋值给dest,并让src和dest都++,如果遇到val就只让src++,直到src走完。这样时间复杂度:O(N)。空间复杂度:O(1)。
4.2
https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/思路:定义dest指向第一个元素,src指向第二个元素,如果src的值等于dest,src++。如果不等于,就让src的值给dest下一个位置的值,并让它们都++。和上面例题思路相似。
4.3
https://leetcode-cn.com/problems/merge-sorted-array/https://leetcode-cn.com/problems/merge-sorted-array/思路:合并数组,可以想到归并。如果排升序:
题目这里合并到一个数组,并保持原有的顺序,题中原有数据是递增的,因此从后开始遍历,取大的放在后面。定义end1和end2分别指向两数组的最后,定义src来地位放置位置。哪个下标指向的元素大就放在src的位置,并让下标和src都++。