0. Intro
图片来源于视频Crash Course Computer Science
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
1.线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
2. 顺序表
数组和顺序表那么像,为什么我们还要单独创建一个线性表的说法,而不是直接用数组呢?
图片来源于视频Crash Course Computer Science
其实在C99之前是没有柔性数组概念的,为了解决C语言的缺陷,即数组长度是固定的,于是人们发明了顺序表
2.1 顺序表的分类
顺序表一般可以分为:
🌿 静态顺序表:使用定长数组存储元素。
🌿 动态顺序表:使用动态开辟的数组存储。
3. 实现线性表接口
3.0 线性表结构体
以下采取动态版
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a;
int size;
int capacity;
}SL, SeqList;
3.1 SeqListInit
初始化
void SeqListInit(SeqList* pq)
{
assert(psl);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
3.2 SeqPrint
打印
void SeqListPrint(SeqList* pq)
{
assert(pq);
for (int i = 0; i < pq->size; ++i)
{
printf("%d ", pq->a[i]);
}
printf("\n");
}
3.3 SeqListCheckCapacity
这个时候我们考虑到一个问题,每次都要判断扩容,为什么不专门写一个函数来复用一下呢?每次需要判断容量可以写一个这样的函数
void SeqListCheckCapacity(SeqList* psl)
{
assert(psl);
if (psl->size == psl->capacity)
{
size_t newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType) * newCapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
psl->a = tmp;
psl->capacity = newCapacity;
}
}
}
3.4 SeqListPushBack
尾插
void SeqListPushBack(SeqList* pq, SLDataType x)
{
assert(pq);
if (pq->size == pq->capacity)
{
size_t newCapacity = pq->capacity == 0 ? 4 : pq->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(pq->a, sizeof(SLDataType) * newCapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
pq->a = tmp;
pq->capacity = newCapacity;
}
}
pq->a[pq->size] = x;
pq->size++;
}
如果有了容量检查函数之后就简化成这样
void SeqListPushBack(SeqList* psl, SLDataType x)
{
assert(psl);
SeqListCheckCapacity(psl);
psl->a[psl->size] = x;
psl->size++;
}
如果有了insert直接简化为如下,这体现了代码的复用
void SeqListPushBack(SeqList* psl, SLDataType x)
{
assert(psl);
SeqListInsert(psl, psl->size, x);
}
3.4 SeqListPopBack
尾删
void SeqListPopBack(SeqList* pq)
{
assert(pq);
if (pq->size > 0)
{
pq->size--;
}
}
3.6 SeqListPushFront
void SeqListPushFront(SeqList* psl, SLDataType x)
{
assert(psl);
SeqListCheckCapacity(psl);
int end = psl->size - 1;
while (end >= 0)
{
psl->a[end + 1] = psl->a[end];
--end;
}
psl->a[0] = x;
psl->size++;
}
如果有了insert函数
void SeqListPushFront(SeqList* psl, SLDataType x)
{
assert(psl);
SeqListInsert(psl, 0, x);
}
3.7 SeqListPopFront
一般的情况都是挪动数据删除,最好不要把最后一个数据放到最前面,因为这样改变了顺序表的顺序,也不要只把头删了,因为这个空间没被释放,仿佛是租了房子不退房,这是不可以的,所以一个一个往前挪会好一点
void SeqListPopFront(SeqList* psl)
{
assert(psl);
if (psl->size > 0)
{
int begin = 1;
while (begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin];
++begin;
}
--psl->size;
}
}
如果写了erase可以直接简化成
void SeqListPopFront(SeqList* psl)
{
assert(psl);
SeqListErase(psl, 0);
}
3.8 SeqListInsert
Insert的时候要注意检查pos,不然的话可能越界
注意由于传进来的参数是无符号的所以,在while判断中如果end是int也会整型提升,当传入的pos为0时也可能会陷入死循环,比如下面
size_t end = psl->size - 1;
while (end >= pos)
{
psl->a[end + 1] = psl->a[end];
--end;
}
所以控制while循环里面写成(end > pos)
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
assert(psl);
if (pos > psl->size)
{
printf("pos 越界:%d\n", pos);
return;
}
SeqListCheckCapacity(psl);
size_t end = psl->size;
while (end > pos)
{
psl->a[end] = psl->a[end - 1];
--end;
}
psl->a[pos] = x;
psl->size++;
}
之前也写过了,其实有了insert之后是可以直接实现尾插和头插的
3.9 SeqListErase
void SeqListErase(SeqList* psl, size_t pos)
{
assert(psl);
assert(pos < psl->size);
size_t begin = pos + 1;
while (begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin];
++begin;
}
psl->size--;
}
尾删和头删是都可以用erase解决的
3.10 SeqListFind
最后是查
int SeqListFind(SeqList* psl, SLDataType x)
{
assert(psl);
for (int i = 0; i < psl->size; ++i)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}
那么整个顺序表的代码放在了我的码云中,有兴趣的话看一下
4. 小试牛刀
接下来利用所学知识解决一些数组题目
注:以下题目均来自leetcode
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int end1=m-1;
int end2=n-1;
int end=end1+end2+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--];
}
}
采用的方式是覆盖,双指针,比拷贝新数组和遍历移动来的高效
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int len=nums.size();
int left=0,right=len-1;;
while(left<=right)
{
if(nums[left]==val)
{
nums[left]=nums[right];
--right;
}
else
{
++left;
}
}
return left;
}
};
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int src =0;
int dst = 0;
int len=nums.size();
if ( len== 0)
return 0;
while (src < len)
{
if (nums[src] == nums[dst])
src++;
else
{
dst++;
nums[dst] = nums[src];
src++;
}
}
return dst + 1;
}
};
注:部分图片来源于Crash Course Computer Science,如有侵权请联系我删除
|