目录
这里要讲的顺序表和链表都属于线性表。
线性表它的逻辑结构是呈线性的,也就是单线串联下去的简单结构。
我们所说的顺序表、链表、栈、队列、字符串都是线性表,在逻辑上是呈线性串成的。
表,除了逻辑结构,还有物理结构。逻辑结构就是设计时候想象的情况,比如顺序表一开始想象的时候是像数组一样按顺序往后排的,链表是一个个串起来的样子.......

我们说是不是线性表也是看它的逻辑结构。
物理结构是实际使用的时候,呈现出的连接方式,可能逻辑结构是线性的,但物理结构就不是了。
顺序表的逻辑结构和物理结构是一致的。
顺序表实质上是数组,但数组是静态的,而顺序表可以是静态的也可以是动态的,它是动态增长的,并且要求里面存放的数据是从左往右连续的。
比如说:创建出一个数组,可以在数组内任何位置存放数据,但是顺序表只能按顺序存入数据。

为什么需要顺序表呢?数组创建只能一次创建固定的空间,比如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 = pq->a[0];
- while (begin<pq->size)
- {
- pq->a[begin] = pq->a[begin+1];
- begin++;
- }
- pq->size--;
- }
顺序表相对来说比较简单,后面的结构会越来越复杂,大家加油把!!!