当前位置:   article > 正文

数据结构篇其一---顺序表

数据结构篇其一---顺序表

前言

数据结构篇---C语言实现数据结构

C语言的基础知识:数组 函数 结构体 指针 动态内存分配

顺序表

从数据结构的角度看待数组

int arr[10];

数组是一个基本的数据结构吗?

这里以一维数组为例。

从数据来看:这个数组存储10个整型数据。

从结构上:数据在内存中连续存储,地址随下标增加而增大。

所以数组也是一个数据结构。

顺序表的概念

顺序:按照一定的顺序,连续。

表:集合,相同事物的集合体。

连续,相同事物?这也符合数组的概念。那么顺序表和数组有什么关系呢?

顺序表底层就是数组,你认为顺序表就是数组也没问题。

已经有数组了为什么单独引入顺序表这个数据结构?

学了数组你能实现以下简单的操作

1.修改数组元素的数据。

2.排序。调用qsort函数,或者独立实现一种排序算法

3.数组传参。

4.用数组下标或者指针访问某个元素。

那么以下的操作你可曾实现过?

1.在数组的某个位置插入某个数据。

2.删除数组某个位置的数据。

3.查找数组中的某个数据。

很遗憾,数组并没有很简单的功能实现这些操作,初学时我们要自己用循环实现。

但我们如今开始学习数据结构了,顺序表就实现了这些算法。

顺序表提供了很多现成的的方法,数组要操作可能需要操作者自己实现的代码,顺序表已经实现了代码,可以立即使用。

结论:顺序表是数组的plus版。

线性表

下面挖掘一下广度。顺序表也只是一棵树的枝叶,我们要对他进行溯源,就是下面的线性表。

我们拆分一下这个词,线性表。

线性:即直线。具有直线的性质,直线是逻辑上的直线,不是物理上的直线。线性表的数据,一定对外表现出一条直线,(我们观察到的)。物理上指内存上的存储,即数据在内存存储是否为一条直线(连续存储)不关心。

表:集合。

线性表是一个(逻辑)线性结构的集合。

顺序表在逻辑上是线性的,所以顺序表是一种线性表。

顺序表在内存空间也是连续的,但线性表不要求空间连续。

那么是线性表但空间上不连续,又是什么数据结构呢?这里留个悬念。

总结:

线性表 逻辑上:线性(连续);物理结构(可连续可不连续)。

顺序表 逻辑上:线性;物理结构 连续。

顺序表的分类

顺序表分为静态顺序表和动态顺序表。

1.静态顺序表

前面说过顺序表的概念了。

限定条件:静态,意味着固定了,不变。

数组在创建时元素个数确定了,这个数组就固定了,称为定长数组。

虽然数组总的元素大小固定,但这数组每个元素不一定全部存入有效的数据,引入size记录有效数据的个数。

因此这里给出静态顺序表的定义:

  1. #define MAX 100 //宏定义明示常量
  2. typedef int SLDataType;//方便替换类型,比如你要存放double类型,只需把int更改为double
  3. typedef struct seqlist {
  4. SLDataType arr[MAX];//定长数组
  5. int size;//记录当前的有效数据
  6. }SL;

2.动态顺序表

限定条件:,灵活,"数组"大小可以变动。

C语言提供了动态内存分配相关的函数,方便程序员灵活操作空间,实现动态增容的目的。由于开辟的空间是连续,我们可以用指针把它当数组处理。

  1. typedef int SLDataType;
  2. typedef struct seqlist {
  3. SLDataType* arr;//需要一个SLDataType类型指向动态开辟内存的指针
  4. int size;//记录有效数据的个数
  5. int capacity;//记录当前开辟的内存大小能存入多少个SLDataType类型的元素个数。
  6. }SL;

size要小于capacity即当前的有效数据小于总的容量大小,如果size==capacity,说明当前动态空间已经用完了,下次要增加值需要动态扩容,即realloc函数。

3.对比两种顺序表,分析优缺点。

动态顺序表和静态顺序表各有其优缺点,下面是对两者的详细对比:

静态顺序表:

优点:
空间确定性:静态顺序表在声明时即分配了固定大小的内存空间,这确保了其存储空间的确定性,便于管理和使用。
内容有效性,虽然是定长数组,但使用者知道静态顺序表大致会用多少空间,利用率高效。
缺点:
空间灵活性差:静态顺序表的大小在初始化时已经确定,无法在运行过程中动态地增加或减少。


动态顺序表:
优点:
空间灵活性高:动态顺序表的大小可以在运行时根据需要动态地增加或减少,这使得它更加灵活,能够适应不同规模的数据存储需求。


缺点:
内存管理复杂:动态顺序表要手动管理内存,包括分配和释放内存空间。如果管理不当,可能会引发内存泄漏问题。
性能上损耗:动态地分配和释放内存,动态顺序表在运行时可能会带来一些额外的开销,如内存分配,内存回收。

总结:看应用场景。
没有孰优孰劣,在数据量固定且空间需求明确的情况下,静态顺序表可能更加合适;而在数据量不确定或需要动态调整存储空间的情况下,动态顺序表可能更为适用。

顺序表的实现

这里以动态顺序表的实现为例,因为其实现相对有难度。

思考一下,我们要实现顺序表的那些功能?

动态顺序表放这儿方便看

  1. typedef int SLDataType;
  2. typedef struct seqlist {
  3. SLDataType* arr;//需要一个SLDataType类型指向动态开辟内存的指针
  4. int size;//记录有效数据的个数
  5. int capacity;//记录当前开辟的内存大小能存入多少个SLDataType类型的元素个数。
  6. }SL;
SL s;//先创建一个顺序表出来

有了一个顺序表,第一步肯定是初始化吧。

接下来要用其实现某些功能了,即增删查改。

增:将数据存入顺序表,可以分头插,尾插,任意位置插入数据。

删:将数据从顺序表删除,有头删,尾删,指定位置删除。

查:即查找顺序表中某个元素是否存在。

改:修改顺序表指定位置的元素。

最后一步,如果顺序表不再使用了,记得销毁,将内存返还给操作系统。

采用的多文件实现,后面会附上源代码

 顺序表的初始化

  1. void SLInit(SL* ps)
  2. {
  3. ps->arr = NULL;
  4. ps->size = ps->capacity = 0;
  5. }

顺序表的销毁

  1. void SLDestroy(SL* ps)
  2. {
  3. if (ps != NULL)//传入结构体指针不为空
  4. {
  5. free(ps->arr);//释放动态开辟的内存
  6. }
  7. //跟初始化操作一样。
  8. ps->arr = NULL;//ps->arr记得原来开辟的起始地址,但现在已经回收内存,需要置空
  9. ps->size = ps->capacity = 0;//当前的有效数据个数和总的数据大小归为0;
  10. }

顺序表的打印

 

为了便于测试过程检验每个函数的各个逻辑是否准确,每次打印出调用函数后的顺序表数据。

  1. //遍历打印一遍顺序表,循环终止条件比较好理解就不赘述了。
  2. void SLPrint(SL* ps)
  3. {
  4. assert(ps);
  5. int i = 0;
  6. for (i = 0; i < ps->size; i++)
  7. {
  8. printf("%d ", ps->arr[i]);
  9. }
  10. printf("\n");
  11. }

顺序表的尾插

  1. void SLPushback(SL* ps, SLDataType x)
  2. {
  3. //1.排除参数为空指针的情况
  4. assert(ps);
  5. //2.检查空间是否足够
  6. //没开辟空间(因为初始化时没给顺序表开辟空间)
  7. //当前开辟的空间满了,需要扩容。
  8. //写一个检查空间并进行扩容的函数
  9. SLCheckCapacity(ps);//后面会说
  10. ps->arr[ps->size] = x;
  11. ++ps->size;
  12. }

动态增容函数设计

  1. void SLCheckCapacity(SL* ps)
  2. {
  3. //检查该结构体指针是否为空
  4. assert(ps);
  5. //没开辟空间和空间不够的条件是ps->size==ps->capacity;
  6. if (ps->size == ps->capacity)
  7. {
  8. //创建一个变量没开辟空间给一个初始的空间大小,否则增容到原来的两倍
  9. int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
  10. //1.既要能开辟空间又能增容的:realloc函数
  11. //2.由于realloc函数可能会失败,返回NULL,创建临时变量存储并检测这个值是否为空指针。
  12. SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity*sizeof(SLDataType));
  13. //检测
  14. if (tmp == NULL)
  15. {
  16. perror("realloc fail");
  17. exit(-1);//异常退出。
  18. }
  19. //开辟或者增容成功了.
  20. ps->arr = tmp;
  21. ps->capacity = newCapacity;//开辟或增容后空间大小要修改。
  22. }
  23. }

顺序表的头插

这里是容量为6的顺序表,要实现头插只需将size前的每个元素逐一往后移。

  1. void SLPushfront(SL* ps, SLDataType x)
  2. {
  3. assert(ps);//判断空指针
  4. SLCheckCapacity(ps);//检查空间大小是否足够
  5. for (int i = ps->size; i>0; i--)
  6. {
  7. ps->arr[i] = ps->arr[i - 1];//终止条件:ps->arr[1]=ps->arr[0];
  8. }
  9. //后移结束,可以进行头插了。
  10. ps->arr[0] = x;
  11. //每次插入一个元素要有效数据加1;
  12. ps->size++;
  13. }

顺序表的尾删

  1. //由于是删除数据,只需一个参数。
  2. //执行删除操作时,顺序表要不为空表。
  3. void SLPopBack(SL* ps)
  4. {
  5. assert(ps);//判断空指针
  6. assert(ps->arr);//判断顺序表不为空表,即当前存在有效数据
  7. --ps->size;//当前有效数据减一即可。
  8. }

顺序表的头删

  1. //由于是删除操作,只需传一个参数。
  2. //只需第二个元素覆盖第一个元素,第三个覆盖第二个,从前往后依次。
  3. void SLPopFront(SL* ps)
  4. {
  5. assert(ps);
  6. int i = 0;
  7. for (i = 1; i<ps->size; i++)
  8. {
  9. ps->arr[i - 1] = ps->arr[i];
  10. //循环终止条件
  11. //ps->arr[0]=ps->arr[1]
  12. //ps->arr[ps->size-2]=ps->arr[size-1];
  13. }
  14. --ps->size;//由于每次删除一个数据,所以记得有效数据减少一。
  15. }

顺序表在指定位置之前的插入

  1. //在pos位置之前插入数据
  2. //由于插入数据,要知道插入到数据是多少,所以需要传参。
  3. //由于插入数据,要检查当前空间是否足够,需要调用SLCheckCapacity函数
  4. //由于指定位置,要知道位置是多少,也需要传参,还需注意其合法性。
  5. void SLInsert(SL* ps, int pos, SLDataType x)
  6. {
  7. assert(ps);
  8. assert(pos >= 0 && pos < ps->size);
  9. SLCheckCapacity(ps);
  10. int i = 0;
  11. for (i = ps->size; i > pos; i--)
  12. {
  13. ps->arr[i] = ps->arr[i - 1];
  14. }
  15. ps->arr[pos] = x;//在指定位置之前插入,即该位置放新数据,原来的位置及之后的数据整体往后挪。
  16. ps->size++;//插入了一个元素,有效数据加一。
  17. }

顺序表在指定位置删除元素

  1. //由于删除数据,对比插入数据,就只要两个参数
  2. void SLErase(SL* ps, int pos)
  3. {
  4. assert(ps);//空指针
  5. assert(pos >= 0 && pos < ps->size);//判断位置区间的有效性
  6. //不用检查空间
  7. int i = 0;
  8. //跟处理头删一样,但这次位置不是0了。
  9. for (i = pos; i < ps->size - 1; i++)
  10. {
  11. ps->arr[i] = ps->arr[i + 1];//最后一次循环,ps->arr[ps->size-2]=ps->size[ps->size-1]
  12. }
  13. --ps->size;
  14. }

修改函数

没什么难度。

  1. void SLmodify(SL* ps, int pos, SLDataType x)
  2. {
  3. assert(ps);
  4. ps->arr[pos] = x;
  5. }

查找函数

  1. //遍历一边当前的有效数据。
  2. //找到了返回下标,没找到(遍历完)返回一个负数,这里是为-1;
  3. int SLFind(SL* ps, SLDataType x)
  4. {
  5. assert(ps);
  6. int i = 0;
  7. for (i = 0; i < ps->size; i++)
  8. {
  9. if (x == ps->arr[i])
  10. {
  11. return i;
  12. }
  13. }
  14. return -1;
  15. }

源代码

seqlist.h

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include<stdio.h>
  3. #include<stdlib.h>
  4. #include<assert.h>
  5. typedef int SLDataType;
  6. //动态顺序表
  7. typedef struct SeqList
  8. {
  9. SLDataType* arr;
  10. int size; //有效数据个数
  11. int capacity; //空间大小
  12. }SL;
  13. //typedef struct SeqList SL;
  14. //顺序表初始化
  15. void SLInit(SL* ps);
  16. //顺序表的销毁
  17. void SLDestroy(SL* ps);
  18. void SLPrint(SL* ps);
  19. //检查空间是否足够
  20. void SLCheckCapacity(SL* ps);
  21. //头部插入删除 / 尾部插入删除
  22. void SLPushBack(SL* ps, SLDataType x);
  23. void SLPushFront(SL* ps, SLDataType x);
  24. void SLPopBack(SL* ps);
  25. void SLPopFront(SL* ps);
  26. //指定位置之前插入/删除数据
  27. void SLInsert(SL* ps, int pos, SLDataType x);
  28. void SLErase(SL* ps, int pos);
  29. //修改函数,查找函数。
  30. void SLModify(SL* ps, int pos,SLDataType X);
  31. int SLFind(SL* ps, SLDataType x);

seqlist.c

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include"seqlist.h"
  3. //顺序表的初始化
  4. void SLInit(SL * ps)
  5. {
  6. ps->arr = NULL;
  7. ps->size = ps->capacity = 0;
  8. }
  9. //顺序表的销毁
  10. void SLDestroy(SL* ps)
  11. {
  12. if (ps != NULL)//传入结构体指针不为空
  13. {
  14. free(ps->arr);//释放动态开辟的内存
  15. }
  16. //跟初始化操作一样。
  17. ps->arr = NULL;//ps->arr记得原来开辟的起始地址,但现在已经回收内存,需要置空
  18. ps->size = ps->capacity = 0;//当前的有效数据个数和总的数据大小归为0;
  19. }
  20. //遍历打印一遍顺序表,循环终止条件比较好理解就不赘述了。
  21. void SLPrint(SL* ps)
  22. {
  23. assert(ps);
  24. int i = 0;
  25. for (i = 0; i < ps->size; i++)
  26. {
  27. printf("%d ", ps->arr[i]);
  28. }
  29. printf("\n");
  30. }
  31. //尾插
  32. void SLPushBack(SL* ps, SLDataType x)
  33. {
  34. //1.排除参数为空指针的情况
  35. assert(ps);
  36. //2.检查空间是否足够
  37. //没开辟空间(因为初始化时没给顺序表开辟空间)
  38. //当前开辟的空间满了,需要扩容。
  39. //写一个检查空间并进行扩容的函数
  40. SLCheckCapacity(ps);//后面会说
  41. ps->arr[ps->size] = x;
  42. ++ps->size;
  43. }
  44. void SLCheckCapacity(SL* ps)
  45. {
  46. //检查该结构体指针是否为空
  47. assert(ps);
  48. //没开辟空间和空间不够的条件是ps->size==ps->capacity;
  49. if (ps->size == ps->capacity)
  50. {
  51. //创建一个变量没开辟空间给一个初始的空间大小,否则增容到原来的两倍
  52. int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
  53. //1.既要能开辟空间又能增容的:realloc函数
  54. //2.由于realloc函数可能会失败,返回NULL,创建临时变量存储并检测这个值是否为空指针。
  55. SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
  56. //检测
  57. if (tmp == NULL)
  58. {
  59. perror("realloc fail");
  60. exit(-1);//异常退出。
  61. }
  62. //开辟或者增容成功了.
  63. ps->arr = tmp;
  64. ps->capacity = newCapacity;//开辟或增容后空间大小要修改。
  65. }
  66. }
  67. //头插
  68. void SLPushFront(SL* ps, SLDataType x)
  69. {
  70. assert(ps);
  71. SLCheckCapacity(ps);
  72. //先让顺序表中已有的数据整体往后挪动一位
  73. for (int i = ps->size; i > 0; i--)
  74. {
  75. ps->arr[i] = ps->arr[i - 1];//arr[1] = arr[0]
  76. }
  77. ps->arr[0] = x;
  78. ps->size++;
  79. }
  80. //尾删
  81. //由于是删除数据,只需一个参数。
  82. //执行删除操作时,顺序表要不为空表。
  83. void SLPopBack(SL* ps)
  84. {
  85. assert(ps);//判断空指针
  86. assert(ps->arr);//判断顺序表不为空表,即当前存在有效数据
  87. --ps->size;//当前有效数据减一即可。
  88. }
  89. //头删
  90. // 由于是删除操作,只需传一个参数。
  91. //只需第二个元素覆盖第一个元素,第三个覆盖第二个,从前往后依次。
  92. void SLPopFront(SL * ps)
  93. {
  94. assert(ps);
  95. int i = 0;
  96. for (i = 1; i < ps->size; i++)
  97. {
  98. ps->arr[i - 1] = ps->arr[i];
  99. //循环终止条件
  100. //ps->arr[0]=ps->arr[1]
  101. //ps->arr[ps->size-2]=ps->arr[size-1];
  102. }
  103. --ps->size;//由于每次删除一个数据,所以记得有效数据减少一。
  104. }
  105. //在pos位置之前插入数据
  106. //由于插入数据,要知道插入到数据是多少,所以需要传参。
  107. //由于插入数据,要检查当前空间是否足够,需要调用SLCheckCapacity函数
  108. //由于指定位置,要知道位置是多少,也需要传参,还需注意其合法性。
  109. void SLInsert(SL* ps, int pos, SLDataType x)
  110. {
  111. assert(ps);
  112. assert(pos >= 0 && pos < ps->size);
  113. SLCheckCapacity(ps);
  114. int i = 0;
  115. for (i = ps->size; i > pos; i--)
  116. {
  117. ps->arr[i] = ps->arr[i - 1];
  118. }
  119. ps->arr[pos] = x;//在指定位置之前插入,即该位置放新数据,原来的位置及之后的数据整体往后挪。
  120. ps->size++;//插入了一个元素,有效数据加一。
  121. }
  122. //由于删除数据,对比插入数据,就只要两个参数
  123. void SLErase(SL* ps, int pos)
  124. {
  125. assert(ps);//空指针
  126. assert(pos >= 0 && pos < ps->size);
  127. //不用检查空间
  128. int i = 0;
  129. //跟处理头删一样,但这次位置不是0了。
  130. for (i = pos; i < ps->size - 1; i++)
  131. {
  132. ps->arr[i] = ps->arr[i + 1];//最后一次循环,ps->arr[ps->size-2]=ps->size[ps->size-1]
  133. }
  134. --ps->size;
  135. }
  136. void SLModify(SL* ps, int pos, SLDataType x)
  137. {
  138. assert(ps);
  139. ps->arr[pos] = x;
  140. }
  141. //遍历一边当前的有效数据。
  142. //找到了返回下标,没找到(遍历完)返回一个负数,这里是为-1;
  143. int SLFind(SL* ps, SLDataType x)
  144. {
  145. assert(ps);
  146. int i = 0;
  147. for (i = 0; i < ps->size; i++)
  148. {
  149. if (x == ps->arr[i])
  150. {
  151. return i;
  152. }
  153. }
  154. return -1;
  155. }

test.c

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include"seqlist.h"
  3. void SLTest(void)
  4. {
  5. SL s;
  6. //初始化
  7. SLInit(&s);
  8. //自己测试各个函数逻辑是否正确。
  9. //顺序表的销毁
  10. SLDestroy(&s);
  11. }
  12. int main()
  13. {
  14. SLTest();
  15. return 0;
  16. }

尾声

 数据结构篇其一---顺序表,结束!

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

闽ICP备14008679号