目录
线性表
这里要讲的顺序表和链表都属于线性表。
线性表它的逻辑结构是呈线性的,也就是单线串联下去的简单结构。
我们所说的顺序表、链表、栈、队列、字符串都是线性表,在逻辑上是呈线性串成的。
表,除了逻辑结构,还有物理结构。逻辑结构就是设计时候想象的情况,比如顺序表一开始想象的时候是像数组一样按顺序往后排的,链表是一个个串起来的样子.......
我们说是不是线性表也是看它的逻辑结构。
物理结构是实际使用的时候,呈现出的连接方式,可能逻辑结构是线性的,但物理结构就不是了。
顺序表
顺序表的逻辑结构和物理结构是一致的。
顺序表实质上是数组,但数组是静态的,而顺序表可以是静态的也可以是动态的,它是动态增长的,并且要求里面存放的数据是从左往右连续的。
比如说:创建出一个数组,可以在数组内任何位置存放数据,但是顺序表只能按顺序存入数据。
为什么需要顺序表呢?数组创建只能一次创建固定的空间,比如arr[10],如果需要存100个数据,但是初始化的时候不知道不要多大空间,创建了1000就浪费了,创建了50就不够,这是数组的缺陷,所以需要顺序表。
顺序表也是有缺陷的:1、如果顺序表初始化空间不够后续增加数据需要增容,但这种动态增容是有性能消耗的(如果后面没有足够的空间,开辟新空间拷贝数据,释放原空间是有消耗的) 。
2、顺序表也是存在空间浪费的,动态扩容一次是扩容原空间两倍的空间的,比如原来是10个字节大小的空间,现在存放的数据满了,要存入再放入1个字节大小的数据,这时就扩容到20字节(2倍),这也满了,再扩容到40字节.......每次都2倍,有可能会造成一定的浪费,但这其实浪费不多,所以一般考虑较少。
3、如果需要在头部插入数据的话,需要挪动数据,将所有数据往后挪动1位,这样做是有代价的,空间复杂度变成了O(N)。
因此,有人发明了新的结构:链表。
链表
链表一般是作用在结构体里面的,它是将一块一块的数据像链条一样串起来。
比如要存一个数据5,它由两部分构成:一个是结构体数据5,后面是一个指针,指向下个节点。
这种结构有他的优势:1、不需要动态扩容,需要插入数据直接创建,指针指向一个节点就连起来了 2、插入头指针不用挪动数据,用头指针指向第一个节点就行了。
但链表也有缺陷,在这里先不说了。
链表的物理结构与逻辑结构不同,不是呈线性的,因为它可以通过指针任意指向节点。
顺序表与链表的区别
上面在介绍的过程中其实就讲了区别了 。
1、顺序表有动态增容,链表没有,所以顺序表经常会有些许空间浪费,链表因为是指针指向节点所以不存在。
2、顺序表头部插入数据需要挪动整个数据,链表不需要,只需要用头指针指向第一个节点就行了。
顺序表具体情况讲解
为了方便起见,创立工程后,用3个文件来实现顺序表的不同功能。
头文件里放结构体定义和函数头,SeqList.c实现顺序表不同功能,test.c用于测试。
顺序表一般都是动态的,静态的用处不大,简单说一下。
//静态顺序表
#define N 100
struct SeqList
{
int a[N];
int size;
};
主要还是来看动态顺序表。
struct SeqList
{
int *a; //指针指向顺序表的头
int size; //size是有效数据的个数
int capacity; //capacity是数据容量
};
*a指针指向顺序表的头, size是有效数据的个数,capacity是数据容量
在这里数据我都定义成了int,但要是后面需要修改类型很麻烦,所以在前面我先重定义一下,方便后面修改:
typedef int SeqDateType;
数据结构在项目中一般是用来存储管理数据的,对内存中管理数据的结构增删查改的接口。
这里面的接口一般有这么4个:
typedef int SeqDateType;
typedef struct SeqList
{
SeqDateType *a; //指针指向顺序表的头
int size; //size是有效数据的个数
int capacity; //capacity是数据容量
}SEQ;
void SeqListInit(SEQ seq);
void SeqListDestory(SEQ seq);
void SeqListPushBack(SEQ seq, SeqDateType x);
void SeqListPushFront(SEQ seq, SeqDateType x);
void SeqListPopBack(SEQ seq);
void SeqListPopFront(SEQ seq);
为了方便,我将结构体类型重定义为SEQ,下面4个接口分别是头插(PushFront)尾插(PushBack),头删(PopFront)尾删(PopBack)。
数据结构里面还有2个常用的接口:初始化(Init)和销毁(Destory),因为顺序表是动态内存开辟的空间,所以用完了需要销毁,否则会内存泄漏。
初始化(调用函数完成,不要在其他地方操作):
注意这里要传结构体变量的地址,将seq改成*pq, 将变量的地址传给Init 。
//SeqList.h
void SeqListInit(SEQ *pq);
void SeqListDestory(SEQ* pq);
void SeqListPushBack(SEQ* pq, SeqDateType x);
void SeqListPushFront(SEQ* pq, SeqDateType x);
void SeqListPopBack(SEQ* pq);
void SeqListPopFront(SEQ* pq);
//test.c
void SeqListTest1()
{
struct SeqList s;
SeqListInit(&s);
SeqListDestory(&s);
}
int main()
{
SeqListTest1();
return 0;
}
//SeqList.c
void SeqListInit(SEQ* pq)
{
assert(pq);
pq->a = NULL;
pq->size = pq->capacity = 0;
}
注意这里用了一个断言函数 assert,防止pq为空指针。
初始化后进行增删查改等操作,完了要销毁,将指针再次置空,空间置0就行了。
void SeqListDestory(SEQ *pq)
{
assert(pq);
free(pq->a);
pq->a = NULL;
pq->size = pq->capacity = 0;
}
接着讲一下头尾增加数据的接口如何设计。
尾插:
在尾部插数据只需要把数据放在下标为size的位置(下标为size实际就是第size+1个数),然后size++就行了。要注意插入的时候size=capacity的情况,需要扩容。
代码如下:
//SeqList.h
void SeqListPushBack(SEQ* pq, SeqDateType x);
void SeqListPrint(SEQ* pq);
//SeqList.c
void SeqListPushBack(SEQ *pq, SeqDateType x)
{
assert(pq);
if (pq->size == pq->capacity)
{
//扩容
int newcapacity = pq->capacity * 2 == 0 ? 4: pq->capacity * 2;
SeqDateType* newA = realloc(pq->a, sizeof(SeqDateType) * newcapacity);
if (newA == NULL)
{
printf("realloc fail\n");
exit(-1);
}
pq->a = newA;
pq->capacity = newcapacity;
}
pq->a[pq->size] = x;
pq->size++;
}
void SeqListPrint(SEQ*pq)
{
assert(pq);
for (int i = 0; i < pq->size; ++i)
{
printf("%d ", pq->a[i]);
}
}
//test.c
void SeqListTest1()
{
struct SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListPrint(&s);
SeqListDestory(&s);
}
int main()
{
SeqListTest1();
return 0;
}
头插:
头插和尾插一样,都要先考虑扩容的问题,因此我们可以把扩容单独做成一个接口分离出来。
头插上面说了需要把所有数据都往后挪动,具体实现就是从后往前将前面一位的值赋给后面一位。
用end指向末尾,每挪动一次,end--往前走一位,直到end<0。
代码如下:
//test.c
//尾插1,2,3,4,5 头插 0 ,0 ,0, 0
void SeqListTest1()
{
struct SeqList s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPushBack(&s, 5);
SeqListPushFront(&s, 0);
SeqListPushFront(&s, 0);
SeqListPushFront(&s, 0);
SeqListPushFront(&s, 0);
SeqListPrint(&s);
SeqListDestory(&s);
}
int main()
{
SeqListTest1();
return 0;
}
//SeqList.c
void SeqCheckCapacity(SEQ* pq)
{
if (pq->size == pq->capacity)
{
//扩容
int newcapacity = pq->capacity * 2 == 0 ? 4 : pq->capacity * 2;
SeqDateType* newA = realloc(pq->a, sizeof(SeqDateType) * newcapacity);
if (newA == NULL)
{
printf("realloc fail\n");
exit(-1);
}
pq->a = newA;
pq->capacity = newcapacity;
}
}
void SeqListPushFront(SEQ *pq, SeqDateType x)
{
assert(pq);
SeqCheckCapacity(pq);
int end = pq->size - 1;
while (end >= 0)
{
pq->a[end+1] = pq->a[end];
end--;
}
pq->a[0] = x;
pq->size++;
}
增加数据会了,删除数据就很简单了。
尾删就直接将有效数据减1就行了。
代码如下:
void SeqListPopBack(SEQ *pq)
{
assert(pq);
assert(pq->size > 0);
--pq->size;
}
头删的话,也是要挪动数据,和上面相反,往前移把第一位挪出去就行了。
代码如下:
void SeqListPopFront(SEQ *pq)
{
assert(pq);
assert(pq->size > 0);
int begin = 0;
while (begin<pq->size)
{
pq->a[begin] = pq->a[begin+1];
begin++;
}
pq->size--;
}
顺序表相对来说比较简单,后面的结构会越来越复杂,大家加油把!!!