当前位置:   article > 正文

数据结构初阶--顺序表和链表

数据结构初阶--顺序表和链表

线性表

1️⃣ 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表: 2️⃣ 顺序表、 3️⃣ 链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

注:

1️⃣线性的意思是数据以连线的方式存储

2️⃣顺序表本质上就是数组。但是在此之上,顺序表要求存储的数据必须是从头开始、连续紧挨着的,不能跳跃间隔

3️⃣链表是用指针把一块一块内存链接起来

图示:

顺序表在逻辑结构上是连续的,物理结构上也是连续的;
链表在逻辑结构上是连续的,但是在物理结构上不是连续的。

顺序表

概念及结构

顺序表是 用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。 顺序表的物理结构和逻辑结构是一致的。

插:创建程序的时候,命名尽量用英文。因为日后在工作中各个源文件链接的时候,中文会出现转换问题,引来不必要的麻烦


顺序表一般可以分为:

1. 静态顺序表:使用 定长数组存储。 特点是如果满了就不让插入。 缺点是空间给多大合适呢?很难确定。N给小了不够用,给大了浪费
2. 动态顺序表:使用 动态开辟的数组存储。

静态顺序表:

  1. //静态的顺序表,并不实用
  2. #define N 100//一发,动全身
  3. struct SeqList//SeqList:顺序表
  4. {
  5. int a[N];
  6. int size;//表示数组中存储了多少个数据
  7. };

注:

用结构体的好处:不仅找到开辟的空间大小,还知道存储的数据个数
以后凡是涉及多个数据的,尽量考虑用结构体

缺点:

因为结构体中定义了数据类型为int,故后续想再插入其它类型的数据就不行了

改进一:可以修改存储的数据类型,插入其它类型的数据

  1. #define N 100
  2. typedef int SLDataType;//int重命名为SLDataType,即顺序表数据类型。以后想插入什么类型就把int改成什么类型
  3. //例如插入double类型,即:typedef double SLDataType;
  4. struct SeqList
  5. {
  6. SLDataType a[N];
  7. int size;
  8. };
  9. void SeqListPushBack(struct SeqList* ps, int x);//SeqListPushBack,即顺序表尾部插入

缺点:

struct SeqList太长,不方便

改进二:将struct SeqList重命名为SL,方便使用

  1. #define N 100
  2. typedef int SLDataType;//int重命名为SLDataType,即顺序表数据类型
  3. typedef struct SeqList
  4. {
  5. SLDataType a[N];
  6. int size;
  7. }SL;
  8. void SeqListPushBack(SL* ps, int x);

动态顺序表:

  1. //顺序表的动态存储
  2. typedef int SLDataType;
  3. typedef struct SeqList
  4. {
  5. SLDataType* array;       //把数组改成指针,意味着我们不再开辟一块固定大小的空间,而是指向一块空间
  6. int size; //表示数组中存储了多少个数据
  7. int capacity; //表示array指向的空间实际能够存数据的空间容量是多大
  8. }SL;

接口函数:

  1. //尾插
  2. void SeqListPushBack(SL* ps, SeqDataType x);
  3. //头插
  4. void SeqListPushFront(SL* ps, SeqDataType x);
  5. //头删
  6. void SeqListPopFront(SL* ps);
  7. //尾删
  8. void SeqListPopBack(SL* ps);
  9. //查找
  10. int SeqListFind(SL* ps, SeqDataType x);
  11. //修改
  12. void SeqListModify(SL* ps, int pos, SeqDataType x);
  13. //中间位置插入
  14. void SeqListInsert(SL* ps, int pos, SeqDataType x);
  15. //中间位置删除
  16. void SeqListErase(SL* ps, int pos);
  17. //初始化
  18. void SeqListInit(SL *ps);
  19. //销毁
  20. void SeqListDestory(SL *ps);
  21. //打印
  22. void SeqListPrint(SL* ps);
  23. //扩容
  24. void SeqCheckCapacity(SL* ps);

注:这里接口函数取名是根据STL里的vector来取的,方便日后学习STL

用处:

在通讯录中,我们输入了某个人的信息。那我们就可以调用所需要的接口函数,把这个信息插入到顺序表当中。顺便提一句,假如插入的信息数据类型是多样的,即结构体类型。那么我们就用 typedef 结构体名 Type;

接口实现

静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开辟多了浪费,开辟少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。

示例一:尾插

Seqlist.h

  1. #pragma once
  2. #include <stdio.h>
  3. #include <assert.h>
  4. #include <stdlib.h>
  5. typedef int SeqDataType; //如果要修改则只需要修改这一处
  6. typedef struct SeqList
  7. {
  8. SeqDataType* a;
  9. int size; //表示a中有多少个有效数据
  10. int capacity; //表示a的空间到底有多大
  11. }SL, SEQ;
  12. //初始化
  13. void SeqListInit(SL* ps);
  14. //尾插
  15. void SeqListPushBack(SL* ps, SeqDataType x);
  16. //扩容
  17. void SeqCheckCapacity(SeqList* ps);
  18. //打印
  19. void SeqListPrint(SL* ps);
  20. //销毁
  21. void SeqListDestory(SL* ps);

test.c:

  1. #include "SeqList.h"
  2. //测试尾插
  3. void TestSeqList1()
  4. {
  5. SL s1;
  6. SeqListInit(&s1);//看解释2
  7. SeqListPushBack(&s1, 1);
  8. SeqListPushBack(&s1, 2);
  9. SeqListPushBack(&s1, 3);
  10. SeqListPushBack(&s1, 4);
  11. SeqListPushBack(&s1, 5);
  12. SeqListPrint(&s1); //1 2 3 4 5
  13.     SeqListDestory(&s1);
  14. }
  15. int main()
  16. {
  17. TestSeqList1();
  18. return 0;
  19. }

SL.c

  1. #include "SeqList.h"//
  2. //对顺序表的初始化
  3. void SeqListInit(SL *ps)//看解释1
  4. {
  5. assert(ps);
  6. ps->a = NULL;
  7. ps->size = ps->capacity = 0;//看解释3
  8. }
  9. //扩容
  10. void SeqCheckCapacity(SL* ps)
  11. {
  12. //如果没有空间或空间不足,则扩容
  13. if (ps->size == ps->capacity)//有两种情况:1.刚初始化,指针为空,size=capacity=0 2.size=capacity≠0
  14. {
  15. //int newcapacity = ps->capacity * 2; //这样会存在问题,当一开始插入数据时,capacity为0,乘2后还是为0
  16. int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; //如果capacity等于0了,则开辟4个空间
  17. SeqDataType* newA = realloc(ps->a, sizeof(SeqDataType*) * newcapacity);//看解释8
  18. if (newA == NULL)//看解释9
  19. {
  20. printf("realloc fail\n");
  21. exit(-1);//看解释10
  22. }
  23. //程序能运行到这儿,说明realloc开辟空间成功
  24. ps->a = newA;//把刚开辟的空间给它
  25. ps->capacity = newcapacity;//把刚扩容的空间给它
  26. }
  27. }
  28. //尾插
  29. void SeqListPushBack(SL* ps, SeqDataType x)
  30. {
  31. assert(ps);
  32. //扩容函数,检查空间是否足够,如果满了则扩容,反之直接插入数据:
  33. SeqCheckCapacity(ps);//看解释6
  34. //插入数据:
  35. ps->a[ps->size] = x;//在有效个数后面放入要插入的数据x
  36. ps->size++;//插入一个数据后有效个数加1
  37. }
  38. //打印
  39. void SeqListPrint(SL* ps)
  40. {
  41. assert(ps);
  42. for (int i = 0; i < ps->size; ++i)
  43. {
  44. printf("%d ", ps->a[i]);
  45. }
  46. }
  47. //销毁
  48. void SeqListDestory(SL* ps)
  49. {
  50. free(ps->a);
  51. ps->a = NULL;//防止a成为野指针
  52. ps->capacity = ps->size = 0;
  53. }

解释:

1️⃣如果void SeqListInit(SL *ps)初始化成功,则调用监视时,显示结果是这样的:

2️⃣SeqListInit(&s1),为何用&s1而不用s1?

s1是我们定义的一个结构体类型变量,如果用s1,那么对应的void SeqListInit(SL ps)中的ps,属于传值调用。然而函数调用后的形参ps不是实参s1,而是s1的临时拷贝,形参的改变无法影响实参。那我们后续搞的尾插,头插,头删...毫无意义

所以我们要改成传址调用,传址调用是形参通过实参传递的地址访问实参的空间,并在其空间进行修改

3️⃣为什么该项目中对结构体成员的访问都是用的->而不是.?

因为我们函数调用采用的是传址调用,所以要用->

4️⃣动态顺序表的尾插是怎么实现的?

下图举例,插入数据5

5️⃣动态顺序表尾插的三种情况:

  • 整个顺序表都没有空间,处于刚初始化,空指针和无空间状态

  • 空间不够,先扩容

  • 空间足够,直接插入数据即可

6️⃣ 为什么会有SeqCheckCapacity(ps)?

因为接下来我们要在顺序表中插入数据,插入前就必须检查空间是否充足。如果充足,就数据就插入,否则要扩容再插入

7️⃣扩容时,我们不是扩一个插一个,效率低。而是一次扩现有容量空间的2倍

8️⃣这里用realloc的原因:

实际上,第一次进来的时候,指针变量为空指针、size=capacity=0。这时我们应该先开辟一块空间,再考虑之后的扩容。开辟空间,我们就需要用到malloc函数,而realloc是对已有空间扩容。但是,如果realloc指向的那块空间是空指针,那么realloc函数的功能等价于malloc函数,即开辟空间

9️⃣newA==NULL则表明上面的realloc分申请空间失败,返回NULL

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/585913
推荐阅读
相关标签