一、线性表
线性表的定义:
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使
用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,
线性表在物理上存储时,通常以数组和链式结构的形式存储。
二、顺序表
1.顺序表的定义
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存
储。在数组上完成数据的增删查改。
2.顺序表的分类
静态顺序表:使用定长数组存储元素。
#define N 100 //数组的最大长度
typedef int SLDataType; //重命名数据类型
struct SeqList
{
SLDataType arr[N]; //使用定长数组来存储数据
int size; //存储数据的个数
}SL;
动态顺序表:使用动态开辟的数组存储。
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* arr;
int size;
int capacity;
}SL;
三、动态顺序表的实现
1.结构的定义
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* arr;
int size;
int capacity;
}SL;
我们将数据类型重命名为SLDataType,这样以后我们管理其他数据类型的时候只需要改一个地方即可。
相对于静态顺序表,动态顺序表多了一个参数capacity,用来记录顺序表当前的容量,当当前的数据个数size和顺序的容量capacity相等时,需要进行扩容。
2.顺序表的初始化
void SeqListInit(SL* psl)
{
assert(psl);
psl->arr = NULL;
psl->size = psl->capacity = 0;
}
在初始化顺序表时,将数据的空间地址置为空,size和capacity置为0,在需要存放数据时再动态开辟空间即可,也可以在初始化时先动态开辟一块空间。
3.检查顺序表的容量
void CheckCapacity(SL* psl)
{
if (psl->size == psl->capacity)
{
int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SLDataType* tmp = (int*)realloc(psl->arr, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
psl->arr = tmp;
psl->capacity = newCapacity;
}
}
当size和capacity相等时,表示数组已经放满了数据,这时就需要进行扩容,我们一般扩容到原来的2倍,
因为扩容多了容易造成空间浪费,扩容少了,会频繁扩容,效率损失,2倍比较合适,1.5倍也可以。我们在动态开辟时,用一个临时变量接收realloc的返回时,这样就可以防止在扩容失败的时候丢失原来的空间。
4.顺序表的销毁
void SeqListDestroy(SL* psl)
{
assert(psl);
free(psl->arr);
psl->arr = NULL;
psl->size = psl->capacity = 0;
}
销毁顺序表时,释放arr数组空间,把size和capzcity置为0 即可。
5.打印顺序表中的数据
void SeqListPrint(SL* psl)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size; i++)
{
printf("%d ", psl->arr[i]);
}
printf("\n");
}
遍历数组,一次打印即可。
6.在头部插入数据
void SeqListPushFront(SL* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl);
int end = psl->size - 1;
while (end >= 0)
{
psl->arr[end + 1] = psl->arr[end];
end--;
}
psl->arr[0] = x;
psl->size++;
}
在头部插入数据时,需要所有数据从后往前依次挪到下一个位置。(注意是从后往前挪,否则数据会被覆盖)
7.在尾部插入数据
void SeqListPushBack(SL* psl, SLDataType x)
{
assert(psl);//断言:防止psl为空
CheckCapacity(psl);//检查容量,空间不够时扩容
psl->arr[psl->size] = x;
psl->size++;
//调用Insert函数
//SLInsert(psl, psl->size, x);
}
在尾部插入时,直接插入即可。
8.在pos位置插入数据
void SeqListInsert(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos <= psl->size);
CheckCapacity(psl);
size_t end = psl->size;
while (end > pos)
{
psl->arr[end] = psl->arr[end - 1];
end--;
}
psl->arr[pos] = x;
psl->size++;
}
在pos位置插入数据时,需要把pos后面的数据依次往后挪动一位。
9.在头部删除数据
void SeqListPopFront(SL* psl)
{
assert(psl);
int begin = 1;
while (begin < psl->size)
{
psl->arr[begin - 1] = psl->arr[begin];
begin++;
}
psl->size--;
}
在头部删除数据时,需要将数据从前往后挪动一位**(注意是从前往后挪动,否则其他数据会被覆盖)**
10.在尾部删除数据
void SeqListPopBack(SL* psl)
{
assert(psl);
psl->size--;
}
如果尾部有数据,size–即可。
11.删除pos位置的数据
void SeqListErase(SL* psl, size_t pos)
{
assert(psl);
int end = pos;
while (end < psl->size - 1)
{
psl->arr[end] = psl->arr[end + 1];
end++;
}
psl->size--;
}
把pos位置后面的数据依次往前挪动即可。
12.查找数据
int SeqListFind(SL* psl, SLDataType x)
{
assert(psl);//断言:防止psl为空
//遍历查找
int i = 0;
for (i = 0; i < psl->size; i++)
{
if (x == psl->arr[i])
return i;
}
return -1;//没有找到返回-1
}
13.修改pos 位置的数据
void SeqListModify(SL* psl, size_t pos, SLDataType x)
{
assert(psl);//断言:防止psl为空
assert(pos <= psl->size);//删除数据的位置要小于psl>size
psl->arr[pos] = x;//修改数据
}
面试题:删除数据需要扩容吗?
我们知道,在插入数据时,在空间不够的时候,我们需要进行扩容,那么,我们在删除数据之后需要把空间返还给内存(缩容)吗?答案是不会。原因如下:
第一:我们缩容之后插入数据有需要重新进行扩容,我们知道,ralloc函数扩容是有代价的:扩容分为原地扩容和异地扩容。
原地扩容:当原来的空间后面有足够的可利用空间时,操作系统会直接把那一块空间交给我们使用,这种情况对效率的影响不大;
异地扩容:当原来空间后面没有足够的可利用的空间时,操作系统会在另外足够的地方为我们开辟一块新的空间,再把我们原来空间中的数据拷贝到新的空间中,再把原来的空间释放掉,这种情况对效率的影响就比较大了。
所以,频繁的扩容使用效率损失的。
第二:缩容也是有代价的,因为缩容和扩容的过程是一样的,分为原地扩和异地扩,会对程序的效率造成一定的影响。
第三:顺序表的申请是连续的一块空间,而free函数不能释放申请空间的一部分,只能一起释放。这就好比,我们团购优惠住酒店,你说你不住了,那时候就不能享受优惠了,因为团购才能有优惠,所以只能同时住和不住。在内存中申请空间和释放空间也是如此,所以我们做不到只释放一部分空间。
四、完整代码
1.SeqList.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* arr;
int size;
int capacity;
}SL;
void SeqListInit(SL* psl);
void CheckCapacity(SL* psl);
void SeqListDestroy(SL* psl);
void SeqListPrint(SL* psl);
void SeqListPushFront(SL* psl, SLDataType x);
void SeqListPushBack(SL* psl, SLDataType x);
void SeqListInsert(SL* psl, size_t pos, SLDataType x);
void SeqListPopFront(SL* psl);
void SeqListPopBack(SL* psl);
void SeqListErase(SL* psl, size_t pos);
int SeqListFind(SL* psl, SLDataType x);
void SeqListModify(SL* psl, size_t pos, SLDataType x);
2.SeqList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void SeqListInit(SL* psl)
{
assert(psl);
psl->arr = NULL;
psl->size = psl->capacity = 0;
}
void CheckCapacity(SL* psl)
{
if (psl->size == psl->capacity)
{
int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SLDataType* tmp = (int*)realloc(psl->arr, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
psl->arr = tmp;
psl->capacity = newCapacity;
}
}
void SeqListDestroy(SL* psl)
{
assert(psl);
free(psl->arr);
psl->arr = NULL;
psl->size = psl->capacity = 0;
}
void SeqListPrint(SL* psl)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size; i++)
{
printf("%d ", psl->arr[i]);
}
printf("\n");
}
void SeqListPushFront(SL* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl);
int end = psl->size - 1;
while (end >= 0)
{
psl->arr[end + 1] = psl->arr[end];
end--;
}
psl->arr[0] = x;
psl->size++;
}
void SeqListPushBack(SL* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl);
psl->arr[psl->size] = x;
psl->size++;
}
void SeqListInsert(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos <= psl->size);
CheckCapacity(psl);
size_t end = psl->size;
while (end > pos)
{
psl->arr[end] = psl->arr[end - 1];
end--;
}
psl->arr[pos] = x;
psl->size++;
}
void SeqListPopFront(SL* psl)
{
assert(psl);
int begin = 1;
while (begin < psl->size)
{
psl->arr[begin - 1] = psl->arr[begin];
begin++;
}
psl->size--;
}
void SeqListPopBack(SL* psl)
{
assert(psl);
psl->size--;
}
void SeqListErase(SL* psl, size_t pos)
{
assert(psl);
int end = pos;
while (end < psl->size - 1)
{
psl->arr[end] = psl->arr[end + 1];
end++;
}
psl->size--;
}
int SeqListFind(SL* psl, SLDataType x)
{
assert(psl);
int i = 0;
for (i = 0; i < psl->size; i++)
{
if (x == psl->arr[i])
return i;
}
return -1;
}
void SeqListModify(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos <= psl->size);
psl->arr[pos] = x;
}
3.test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void TestSeqList1()
{
SL sl;
SeqListInit(&sl);
SeqListPushFront(&sl, 1);
SeqListPushFront(&sl, 2);
SeqListPushFront(&sl, 3);
SeqListPushFront(&sl, 4);
SeqListPrint(&sl);
SeqListPushBack(&sl, 5);
SeqListPushBack(&sl, 6);
SeqListPrint(&sl);
SeqListInsert(&sl, 0, 7);
SeqListPrint(&sl);
}
void TestSeqList2()
{
SL sl;
SeqListInit(&sl);
SeqListPushFront(&sl, 1);
SeqListPushFront(&sl, 2);
SeqListPushFront(&sl, 3);
SeqListPushFront(&sl, 4);
SeqListPushFront(&sl, 5);
SeqListPushFront(&sl, 6);
SeqListPrint(&sl);
SeqListPopFront(&sl);
SeqListPopFront(&sl);
SeqListPrint(&sl);
SeqListPopBack(&sl);
SeqListPrint(&sl);
SeqListErase(&sl, 0);
SeqListPrint(&sl);
int ret = SeqListFind(&sl, 2);
printf("%d\n", ret);
SeqListModify(&sl, 1,10);
SeqListPrint(&sl);
}
int main()
{
TestSeqList2();
return 0;
}
五、顺序表的缺陷
顺序表存在以下缺陷:
\1. 中间/头部的插入删除,时间复杂度为O(N)
\2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
\3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
|