星光不负赶路人
江河眷顾奋楫者
🎥烟雨长虹,孤鹜齐飞的个人主页
🔥个人专栏
期待小伙伴们的支持与关注!!!
线性表的简介#
线性表(linearlist):是n个具有相同特性的数据元素的有限序列。线性表是⼀种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串。 线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
目录
线性表的简介#
顺序表的概念与结构
概念#
结构#
顺序表的接口实现
定义顺序表的结构体#
初始化和动态内存的开辟#
顺序表的头插尾插
顺序表的尾插#
顺序表的头插#
顺序表的头删尾删
顺序表的尾删#
顺序表的头删#
顺序表的指定插入删除
顺序表的指定插入#
顺序表的指定删除#
顺序表在解题中的运用
移除元素
顺序表的概念与结构
概念#
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构
一般情况下采用数组存储,在数组上完成数据的增删查改
日常生活中我们的手机通讯录、聊天软件界面就是通过顺序表实现的
结构#
<1>静态顺序表:使用定长数组存储元素
静态顺序表一旦空间不够用了还需要手动调节,很不方便
<2>动态顺序表:使用动态开辟的数组存储元素
动态顺序表可以通过realloc来调节空间的大小,无需手动
静态顺序表和动态顺序表的优劣:
静态顺序表#
给定数组的长度
优点:
<1>创建空间时为静态开辟,不用malloc函数,代码相对简单
缺点:
<1>若空间小了,会导致后续的数据存储失败
<2>若空间大了,会导致空间的大量浪费
动态顺序表#
动态开辟的数组存储元素
优点:
<1>动态开辟空间,使用相对灵活,相比于静态开辟空间也可以减少空间浪费
缺点:
<1>因为要用到malloc开辟内存,代码相对复杂
由于静态顺序表实用价值极低,实现思路上不如动态顺序表
且会实现动态顺序表的话实现静态顺序表也没有太大的问题
因此在这里只给大家详细讲解一下动态顺序表的实现
顺序表的接口实现
今天我将带领大家去学习顺序表的动态实现:
<1>从头部插入数据
<2>从尾部插入数据
<3>从头部删除数据
<4>从尾部删除数据
<5>从指定位置插入数据
<6>从指定位置删除数据
现在到了我们写代码的环节啦
我们依然是像这种代码量多的项目最好分模块写代码
定义顺序表的结构体#
#include<stdio.h> //标准的输入输出 #include<stdlib.h>//开辟动态空间的 #include<assert.h>//断言 typedef int SLDataType; //对变量类型的重命名 typedef struct SeqList { SLDataType* str; //指向开辟的动态空间 int capacity; //记录当前顺序表的空间大小 int size; //记录当前顺序表的元素个数 }SL; //对结构体的重命名
我们使用 typedef 的好处就是减少类型需求的变化带来修改上的麻烦
初始化和动态内存的开辟#
void SLInit(SL* h); //初始化 void SLCheckCapacity(SL* h);//动态内存开辟
void SLInit(SL* h) { h->str = NULL; h->capacity = h->size = 0; } void SLCheckCapacity(SL* h) { if (h->size == h->capacity) { //三目运算符判断顺序表的扩容 //如果空间大小为0就开4个SLDataType类型的空间 //若不够则扩大原来的二倍 int newCapacity = h->capacity = 0 ? 4 : 2 * h->capacity; SLDataType* tmp = (SLDataType*)malloc(h->str, newCapacity * sizeof(SLDataType)); //断言防止开辟失败 assert(tmp); h->str = tmp; h->capacity = newCapacity; } }
可能有小伙伴不理解为啥 void SLInit(SL* h) 传的是指针
我们来看
因为现在是传值调用:形参是实参的拷贝,出了函数就会被销毁
而我们为了修改实参的数据应该选用传址调用-指针
顺序表的头插尾插
顺序表的尾插#
对于表尾插入元素,需要先判断表是否已满,如果未满,则将元素插入表尾
void SLPushBack(SL* h, SLDataType x) { //判断是否扩容 SLCheckCapacity(h); //空间足够,直接插入 h->str[h->size++] = x; }
在用我们的SLPrint()函数将尾插结果测试一下
void SLPrint(SL* h) { //遍历顺序表 for (int i = 0; i < h->size; i++) { printf("%d ", h->str[i]); } printf("\n"); }
这样我们的尾插就完成啦
顺序表的头插#
对于头插入元素,需要先将首位置及其之后的元素后移,然后再将要插入的元素放入头部
void SLPushFront(SL* h,SLDataType x) { //判断是否扩容 SLCheckCapacity(h); //原顺序表中的元素全部往后挪动 for (int i = h->size; i > 0; i--) { h->str[i] = h->str[i - 1]; } //将待插入元素放在首位 h->str[0] = x; //元素个数加一 h->size++; }
这样我们的头插也就完成啦
顺序表的头删尾删
顺序表的尾删#
对于删除表尾元素,直接将表中的元素个数减一即可
void PopBack(SL* h) { //判断顺序表是否为空 assert(h); assert(h->size); //顺序表不为空 //减掉元素个数 h->size--; }
我们发现末尾的三个元素被删除了,这样我们的尾删接口也就完成啦
顺序表的头删#
对于删除表中首元素,需要将首位置之后的元素前移,最后将表中的元素个数减一
void PopFront(SL* h) { assert(h); assert(h->size); //用后面的元素覆盖前一个元素 for (int i = 0; i < h->size - 1; i++) { h->str[i] = h->str[i + 1]; } h->size--; }
这样我们的头删也就完成啦!!!是不是难不倒你呢?
顺序表的指定插入删除
顺序表的指定插入#
以下是将元素6插入指定位置3的图片模拟
对于在表中任意位置插入元素,需要先将插入位置及其之后的元素后移,然后再将要插入的元素放入合适的位置
void SLInsert(SL* h, int pos, SLDataType x) { //判断动态内存是否开辟成功 assert(h); //规定pos的范围区间 assert(pos>=0&&pos<=h->size); SLCheckCapacity(h); //pos之后的数据全部往后挪动一位,pos空出来 for (int i = h->size; i > pos; i--) { h->str[i] = h->str[i - 1]; } h->str[pos] = x; h->size++; }
我们发现7和0以及10已经插入了我们指定的位置啦
顺序表的指定删除#
以下是删除指定位置3的图片模拟
对于删除表中任意元素,需要先找到要删除的元素的位置,然后将该位置之后的元素前移,最后将表中的元素个数减一
void PopErase(SL* h, int pos) { //判断动态内存是否开辟成功 assert(h); assert(pos >= 0 && pos <= h->size); //pos后面的数从后往前移动 for (int i = pos; i < h->size-1; i++) { h->str[i] = h->str[i + 1]; } h->size--; }
我们发现0和1以及2已经在我们指定的位置删除啦
顺序表的动态内存销毁
void SeqListFree(SL* h) { free(h->str); //释放动态申请的内存空间 h->str = NULL;//置空指针 h->size = 0; h->capacity = 0; }
为了避免发生程序的内存泄露问题,在不使用空间的时候记得释放哦
以下是全部代码的实现:
#include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int SLDataType; typedef struct SeqList { SLDataType* str; int capacity; int size; }SL; void SLInit(SL* h); void SLCheckCapacity(SL* h); void SLPrint(SL* h); void SLPushBack(SL* h, SLDataType x); void SLPushFront(SL* h, SLDataType x); void PopBack(SL* h); void PopFront(SL* h); void SLInsert(SL* h, int pos, SLDataType x); void PopErase(SL* h, int pos); void SeqListFree(SL* h); void SLInit(SL* h) { h->str = NULL; h->capacity = h->size = 0; } void SLCheckCapacity(SL* h) { if (h->size == h->capacity) { int newCapacity = h->capacity = 0 ? 4 : 2 * h->capacity; SLDataType* tmp = (SLDataType*)malloc(h->str, newCapacity * sizeof(SLDataType)); assert(tmp); h->str = tmp; h->capacity = newCapacity; } } void SLPrint(SL* h) { for (int i = 0; i < h->size; i++) { printf("%d ", h->str[i]); } printf("\n"); } void SLPushBack(SL* h, SLDataType x) { SLCheckCapacity(h); h->str[h->size++] = x; } void SLPushFront(SL* h,SLDataType x) { SLCheckCapacity(h); for (int i = h->size; i > 0; i--) { h->str[i] = h->str[i - 1]; } h->str[0] = x; h->size++; } void PopBack(SL* h) { assert(h); assert(h->size); h->size--; } void PopFront(SL* h) { assert(h); assert(h->size); for (int i = 0; i < h->size - 1; i++) { h->str[i] = h->str[i + 1]; } h->size--; } void SLInsert(SL* h, int pos, SLDataType x) { assert(h); assert(pos>=0&&pos<=h->size); SLCheckCapacity(h); for (int i = h->size; i > pos; i--) { h->str[i] = h->str[i - 1]; } h->str[pos] = x; h->size++; } void PopErase(SL* h, int pos) { assert(h); assert(pos >= 0 && pos <= h->size); for (int i = pos; i < h->size-1; i++) { h->str[i] = h->str[i + 1]; } h->size--; } void SeqListFree(SL* h) { free(h->str); h->str = NULL; h->size = 0; h->capacity = 0; } void CeShi() { SL s1; SLInit(&s1); } int main(void) { CeShi(); system("pause"); }
顺序表在解题中的运用
移除元素
题目链接:移除元素
题目描述#
给你一个数组
nums
和一个值val
,你需要 原地 移除所有数值等于val
的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用
O(1)
额外空间并 原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明#
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝 int len = removeElement(nums, val); // 在函数里修改输入数组对于调用者是可见的。 // 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。 for (int i = 0; i < len; i++) { print(nums[i]); }示例 1:
输入:nums = [3,2,2,3], val = 3 输出:2, nums = [2,2] 解释:函数应该返回新的长度2
, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2 输出:5, nums = [0,1,3,0,4] 解释:函数应该返回新的长度5
并且 nums 中的前五个元素为0 1 3 0 4
注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
思路:
<1>定义两个快慢指针,都指向开头
<2>如果没有遇到val两个指针就一起走,并且left将当前的值给right指针
<3>遇到val时,left先去寻找共同的目标(不是val的数)并把它给right
<4>这样返回right就是移除后的新长度
class Solution { public: int removeElement(vector<int>& nums, int val) { int left = 0; int right = 0; int n = nums.size();//计算数组大小 while(left<n) { if(nums[left] != val) { nums[right++] = nums[left++]; } else { left++; } } return right; } };
总结:
<1>编写代码量大的项目,要学会分模块写
<2>动态内存在不适使用的情况下,记得销毁,避免内存泄露
<3>顺序表的本质就是数组,只不过增加了增删查改的功能
<4>在静态分配时,由于数组的大小和空间是固定的,一旦空间占满,就无法再新增数据,否则会导致数据溢出
<5>在动态分配时,存储数组的空间在程序执行过程中会动态调整大小,当空间占满时,可以另行开辟更大的存储空间来储存数据