一.概念及结构
1.概念
静态顺序表弊端较大,如果空间过小则不足,空间过大则浪费。
(2)动态
对数组进行扩容处理,和动态内存管理类似。
2.区分:结构体和柔性数组
结构体:a指向这块空间
柔性数组:size capacity在同一空间内,后续数据在后面添加
3.结构体传参
结构体传参要传地址,否则实参不会发生变化
二.顺序表接口实现
1.头文件
#pragma once
#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* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);
void SLCheckCapacity(SL* ps);
void SLPushBack(SL* ps, SLDataType x);//尾插
void SLPopBack(SL* ps);//尾删
void SLPushFront(SL* ps, SLDataType x);//头插
void SLPopFront(SL* ps);//头删
void SLInsert(SL* ps, int pos, SLDataType x);//插入
void SLErase(SL* ps, int pos);//删除
int SLFind(SL* ps, SLDataType x);//寻找
2.初始化结构体
#include "SeqList.h"//包含头文件,定义参数为4
//初始化
void SLInit(SL* ps)
{
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);//开辟一段4字节的空间
if (ps->a == NULL)//判断是否开辟成功
{
perror("malloc fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
3.尾插
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
if (ps->capacity = ps->size)//判断是否要进行扩容操作
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);//扩容
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
//ps->a[ps->size] = x;
//ps->size++;
ps->a[ps->size++] = x;//上写两种写法都可以,下面这个写法更简洁
//就是ps先指向size也就是最后一个位置的下一位,然后size++
}
小bug
这里有一点需要注意
上面那个图会出现debug,因为他的扩容是存在问题的,这里ps->capacity前面定义为4字节,乘2变为8字节,但是我们需要的是8个SLDataType(整形)位共计32字节,这里就产生了越界。
我们怎么去发现这个问题的:
free的断点bug问题
调试的过程中,我们发现free处出现了断点(见二.10),free处出现断点就有两种情况:1.出现野指针;2.出现越界情况,而多半是第二种情况,所以我们发现了是越界情况产生的。
4.尾删
void SLPopback(SL* ps)
{
//ps->a[ps->size-1]=0;
ps->a[ps->size--];
}
这里屏蔽掉的那行代码可以选择不要,因为顺序表本来就是记录连续的size个数据,有效数据只有size个,不必规0。
测试
这里出现了一个debug,这是由于删的太多导致的,size为负,就出现问题了,因此我们要对尾删做出修改。
暴力检查和温柔检查
void SLPopback(SL* ps)
{
//暴力检查
assert(ps->size > 0);
//ps->a[ps->size-1]=0;
ps->a[ps->size--];
}
使用断言检查,一旦出现size<=0的情况,立即终止报错。
void SLPopback(SL* ps)
{
//温柔检查
if (ps->size == 0)
return;
//ps->a[ps->size-1]=0;
ps->a[ps->size--];
}
ps->size为0时自动停止,不会报错
5.头插
引入checkcapacity函数,节省代码空间(就是为了避免开辟空间失败)
下面是头插部分
void SLPushfront(SL* ps,SLDataType x)
{
assert(ps);
//SLCheckCapacity(ps);
SLDataType end = ps->size - 1;//指向最后一个位置
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];//把end位置给给下一个位置,腾出来位置给开头
end--;
}
ps->a[0] = x;
ps->size--;
}
6.头删
void SLPopfront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
SLDataType begin = 1;
while (begin < ps->size)//后面覆盖前面的,一点一点覆盖
{
ps->a[begin - 1] = ps->a[begin];
++begin;//往后移一个位置
}
ps->size--;//减少一个元素size
}
测试
7.插入数据
void SLInsert(SL* ps, int pos, SLDataType x)//在pos的下一个位置插入数据
{
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++;
}
插和尾插其实也可以通过上述的方法解决:
SLInsert(ps, 0, x); //头插
SLInsert(ps, ps->size, x); //尾插
8.删除数据
void SLErase(SL* ps, int pos)//删除pos后面的元素
{
assert(ps);
assert(pos >= 0 && pos < ps -> size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin-1] = ps->a[begin];//pos元素后面的元素是被覆盖的,pos是从1开始到第pos个位置
++begin;
}
ps->size--;
}
9.查找数据
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
if (ps->a[i] == x)
return i;
}
return -1;
}
10.释放空间
void SLDestroy(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
11.复杂度
尾插/尾删N个数据的时间复杂度:O(N)
头插/头删N个数据的时间复杂的:O(N^2)
可以看出顺序表更适合尾插尾删
三.realloc详解
1.基本概念
2.realloc的原地扩容和异地扩容
(1).原地扩容
开辟对应的空间没有被占用,则进行原地扩容。
地址一样,为原地扩容
(2).异地扩容
开辟对应的空间被占用,则另开辟一块更大的没有被占用的空间,将原数据拷贝到新空间,并释放原先的空间。
地址不同,为异地扩容。
3.realloc的扩容原理
这里很明显出现了越界访问,显示系统崩溃。
当我们通过ptr3将代码的realloc范围控制到5后,再次realloc到20,发现地址不变,说明是在同一个地址上,也就是说ptr3的操作并没有将空间销毁,而是将数据回收。
通过上下两图的对比可以发现,realloc是只扩不缩的,ptr3那行代码只是将多余空间的使用权收回,而并非销毁,这样可以保证再扩的时候还能原地扩容。
四.OJ实战
1.移除元素
力扣https://leetcode.cn/problems/remove-element/
这里我们来画图解决这个问题:
双向指针解法,将该数组存到两个指针src和dst内,逢2,src就往下走一个,dst不动,直到src遍历整个数组。
int removeElement(int* nums, int numsSize, int val){
int src=0,dst=0;
while(src<numsSize)
{
if(nums[src]!=val)
{
nums[dst++]=nums[src++];
}
else
{
src++;
}
}
return dst;
}
2.删除有序数组中的重复项
力扣https://leetcode.cn/problems/remove-duplicates-from-sorted-array/
数据重复,则跳过该数据;数据不重复,则在下一个位置录入该数据。
int removeDuplicates(int* nums, int numsSize){
int hgs=1,sct=0;
while(hgs<numsSize)
{
if(nums[hgs]==nums[sct])
{
hgs++;
}
else if(nums[hgs]!=nums[sct])
{
nums[++sct]=nums[hgs++];
}
}
return sct+1;
}
3.合并两个有序数组
力扣https://leetcode.cn/problems/merge-sorted-array/
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
int end1=m-1,end2=n-1;
int hgs=m+n-1;
while(end1>=0&&end2>=0)
{
if(nums1[end1]>nums2[end2])
{
nums1[hgs--]=nums1[end1--];
}
else
{
nums1[hgs--]=nums2[end2--];
}
}
while(end2>=0)
{
nums1[hgs--]=nums2[end2--];
}
}
五.顺序表的缺陷
1.中间头部插入删除数据,需要挪动数据,效率低下;
2.空间不够,扩容。扩容会有一定的消耗,还可能会有一定的空间浪费。