当前位置:   article > 正文

【C语言】实现动态顺序表及详细讲解_c语言的动态顺序表

c语言的动态顺序表

目录

一、线性表

二、顺序表

1.概念及结构

2.静态 or 动态

3.接口实现

初始化

销毁

打印

尾插 及 容量检查

尾删

头插

头删

查找

指定pos下标插入

指定pos下标删除

总结


前言

数据结构的常见线性表,分别是顺序表,链表,栈,队列

本篇给大家带来动态顺序表的实现和讲解
 

一、线性表

线性表是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。

二、顺序表

1.概念及结构

顺序表是用一段物理地址连续的存储单元,从头开始连续依次存储数据元素的线性结构,一般采用数据存储。   

2.静态 or 动态

1.静态顺序表:使用定长数组存储

2.动态顺序表:使用动态开辟的数组存储

静态

 

动态

 

 

因为静态顺序表的长度是固定的,因此数组满了需要手动更改N,不然就如上图数据插入不成功,而动态顺序表就解决了这个问题。

 

 动态顺序表当容量满了之后,会进行自动扩容

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

3.接口实现

一般顺序表拥有这些接口

1. 初始化 SLInit

2. 销毁 SLDestroy

3. 容量检查 SLCheckCapacity

4.尾插 SLPushBack

5. 打印 SLPrint

6. 尾删 SLPopBack

7. 头插 SLPushFront

8. 头删 SLPopFront

9. 查找 SLFind

10. 指定下标插入 SLInsert

11. 指定下标插入 SLErase

顺序表动态存储结构

  1. // 动态顺序表
  2. typedef struct SeqList
  3. {
  4. SLDataType* a; // 动态开辟的数组的地址
  5. int size; // 有效数据的个数
  6. int capacity; // 容量空间的大小
  7. }SL;

初始化

  1. // 顺序表初始化
  2. void SLInit(SL* pa);
  1. void SLInit(SL* pa)
  2. {
  3. assert(pa); // 传入的地址不能是NULL
  4. pa->a = NULL;
  5. pa->size = pa->capacity = 0;
  6. }

 

注意我们需要地址传递,而不是值,因为我们值传递对实参没有任何改变,只是实参的临时拷贝,改变的是形参的内容。

通过地址传递初始化成功

销毁

  1. // 顺序表销毁
  2. void SLDestroy(SL *pa);

实现思路:

  1. free掉指向动态开辟空间的指针,目的是把用了的空间还回去,避免内存泄漏
  2. 把指向动态开辟空间的指针置为NULL,目的是防止野指针
  3. 把表有效个数的size和表容量的capacity都置为0

打印

  1. // 顺序表打印
  2. void SLPrint(SL* pa);

 遍历一遍顺序表,同时输出顺序表中的各个值

  1. // 打印
  2. void SLPrint(SL* pa)
  3. {
  4. assert(pa);
  5. for (int i = 0; i < pa->size; i++)
  6. {
  7. printf("%d ", pa->a[i]);
  8. }
  9. printf("\n");
  10. }

尾插 及 容量检查

  1. // 顺序表容量检查
  2. void SLCheckCapacity(SL* pa);
  3. // 顺序表尾插
  4. void SLPushBack(SL* pa, SLDataType x);

顺序表的尾插特别简单,就是最后面增加一个数据

但是存放数据之前,顺序表必须是未满状态下,否则就无法存储

所以我们可以在尾删前,做一个容量检查的函数

容量检查函数就两个功能

  1. 检查顺序表是否满了
  2. 满了就进行扩容

具体实现如下:

  1. // 容量检查
  2. void SLCheckCapacity(SL* pa)
  3. {
  4. assert(pa);
  5. if (pa->size == pa->capacity)
  6. {
  7. int newCapacity = pa->capacity == 0 ? 4 : pa->capacity * 2;
  8. SLDataType* tmp = (SLDataType*)realloc(pa->a, newCapacity * sizeof(SLDataType));
  9. if (tmp == NULL)
  10. {
  11. printf("扩容失败");
  12. exit(-1);
  13. }
  14. pa->a = tmp;
  15. pa->capacity = newCapacity;
  16. }
  17. }
  • 判断有效数据和容量大小是否相等,相等就满了
  • 相等有会两种情况,1.就是刚初始化完    2.就是插入数据时
  • 刚初始化完我们可以先给它一点空间,如果是插入时满了可以在原有容量的基础上扩容2倍,这样可以减少烦琐
  • 这里可以用扩容函数realloc,它和malloc功能类似都是在堆(heap)上面开辟空间,但是它可以在扩容的基础上重新扩容,可以保存之前扩容的数据,malloc的话就是不能保存之前扩容的数据,可以看上图的测试,还有一个特性就是扩容的指针是NULL,就实现和malloc一样的功能,第二个参数单位是字节数,不是个数
  • 如果扩容的指针等于NULL,NULL就是代表扩容失败了,失败就提示下然后异常退出,exit(-1):异常退出,exit(0):正常退出
  • 扩容成功就把扩容好的指针赋值给指向数组的地址,容量大小更新为扩容好的容量大小

扩容函数实现完成后,我们开始实现尾插函数

先要考虑尾插的基本逻辑:

   1. 需要容量检查,我们可以直接调容量检查函数

   2. 容量够,我们就在size位置插入数据,然后size递增即可

      因为数组下标从0开始,size位置是最后一个数据的后面

具体实现如下:

  1. // 尾插
  2. void SLPushBack(SL* pa, SLDataType x)
  3. {
  4. assert(pa);
  5. SLCheckCapacity(pa);
  6. pa->a[pa->size] = x;
  7. pa->size++;
  8. }

 大家写完一个接口,可以测试下,不要一个劲的往后写,出题了问题,调试很麻烦

 

如上图,扩容了2次,第1次:初始化后扩容为4,第2次:容量满了扩容为8,尾插5次也成功插入

尾删

  1. // 顺序表尾删
  2. void SLPopBack(SL* pa);

尾删就是把size--吗?

尾删需要注意这几点:

  1. 尾删数据后要不要释放空间?数据要不要改为0
  2. 每次尾删前要不要判断size个数

第一个问题:

  1. 顺序表删除数据后不需要释放空间,也不能释放空间,因为free只能释放指向开辟空间的指针(首地址),并不能释放其它地方的空间
  2. 不需要改为0,没有意义,删除数据之后,下次访问这块空间肯定是插入数据,新的数据会覆盖掉

第二个问题:

        需要判断,如果删除次数大于size个数,size的值就会为负数,就会导致越界,插入数据就会在开辟空间的前面插入,free这个开辟的空间,就会内存报错,而且你在越界的地方插入的数据,无法显示。

解决方法:只需要判断size的个数大于0,才能尾删,这里我用的是assert(断言),需要包含头文件assert.h,条件为真就正常,条件为假,就报错并且会在控制台打印出报错的位置。

具体实现如下:

  1. // 尾删
  2. void SLPopBack(SL* pa)
  3. {
  4. assert(pa);
  5. assert(pa->size > 0); // size个数判断
  6. pa->size--;
  7. }

测试尾删正常

头插

  1. // 顺序表尾插
  2. void SLPushBack(SL* pa, SLDataType x);

头插就没像头插那么容易,因为顺序表是数组实现的,往往我们在数组第一个位置插入数据,比较麻烦

头插的思路:

  1. 和尾插一样做容量检查

  2. 需要把所有的数据都向后移动一位,就是下标0 - size-1,注意是最后一个数先移动从后往前,如果从前开始就会造成数据覆盖

  3. 把插入的数据放到下标0位置

具体实现如下:

  1. // 头插
  2. void SLPushFront(SL* pa, SLDataType x)
  3. {
  4. assert(pa);
  5. SLCheckCapacity(pa);
  6. for (int end = pa->size - 1; end >= 0; end--)
  7. {
  8. pa->a[end + 1] = pa->a[end];
  9. }
  10. pa->a[0] = x;
  11. pa->size++;
  12. }

扩容和头插都测试正常

头删

  1. // 顺序表头删
  2. void SLPopFront(SL* pa);

头删和头插类似,都需要移动数据

头删的思路:

   1. 删除前做判断,size必须大于0

   2. 把下标1 - size-1的数向前移动,注意是第一个数据先移动从前往后,如果从后面开始就           会造成数据覆盖

   3. size--

具体实现如下:

  1. // 头删
  2. void SLPopFront(SL* pa) {
  3. assert(pa);
  4. assert(pa->size > 0);
  5. for (int begin = 1; begin < pa->size; begin++)
  6. {
  7. pa->a[begin - 1] = pa->a[begin];
  8. }
  9. pa->size--;
  10. }

测试头删正常

查找

  1. // 顺序表查找数据
  2. int SLFind(SL* pa, SLDataType x);

查找顺序表中的某个数据,找到就返回这个数据下标,没有找到则就返回-1

只需要遍历一遍顺序表,判断下要找的数据和顺序表当前位置的值是否相等

具体实现如下:

  1. // 查找某个数,返回下标
  2. int SLFInd(SL* pa, SLDataType x)
  3. {
  4. assert(pa);
  5. for (int i = 0; i < pa->size; i++)
  6. {
  7. if (pa->a[i] == x)
  8. return i;
  9. }
  10. return -1;
  11. }

测试正常情况和未找到的情况

指定pos下标插入

  1. // 顺序表指定下标插入
  2. void SLInsert(SL* pa, int pos, SLDataType x);

这里和头插类似,只不过移动区间变了,但是要注意pos不能随便插入,我们也需要判断

指定下标插入的思路:

  1. 判断只能在0 - size的下标插入,这里pos等于size,就是尾插
  2. 容量检查
  3. 从pos开始到最后一个数据,全部向后移动一位,同样是最后一个数据先移动从后往前,如果从前开始就会造成数据覆盖
  4. 把新数据放入pos的位置

具体实现如下:

  1. // 指定下标插入
  2. void SLInsert(SL* pa, int pos, SLDataType x)
  3. {
  4. assert(pa);
  5. assert(pos >= 0 && pos <= pa->size); // 插入的下标范围在0 - size
  6. SLCheckCapacity(pa);
  7. for (int end = pa->size - 1; end >= pos; end--)
  8. {
  9. pa->a[end + 1] = pa->a[end];
  10. }
  11. pa->a[pos] = x;
  12. pa->size++;
  13. }

  测试pos下标插入正常 

指定pos下标插入实现后,我们可以对他进行复用,用它就可以实现头插和尾插功能

复用后的头插实现

  1. // 头插
  2. void SLPushFront(SL* pa, SLDataType x)
  3. {
  4. SLInsert(pa, 0, x);
  5. }

复用后的尾插实现

  1. // 尾插
  2. void SLPushBack(SL* pa, SLDataType x)
  3. {
  4. SLInsert(pa, pa->size, x);
  5. }

 测试复用SLInsert后的头插和尾插测试正常

指定pos下标删除

  1. // 顺序表指定下标删除
  2. void SLErase(SL* pa, int pos);

这里和头删类似,只不过移动区间变了,但是要注意pos不能随便删除,我们也需要判断

指定下标插入的思路:

  1. 对size进行判断,必须要大于0,否则会越界
  2. 判断只能在0 - size-1的下标删除
  3. 从pos+1开始到最后一个数据,全部向前移动一位第一个数据先移动从前往后,如果从后开始就会造成数据覆盖
  4. size--

具体实现如下:

  1. // 指定下标删除
  2. void SLErase(SL* pa, int pos)
  3. {
  4. assert(pa);
  5. assert(pa->size > 0); // size大于0才可以删除
  6. assert(pos >= 0 && pos < pa->size); // 删除的范围下标0 - size-1
  7. for (int begin = pos + 1; begin < pa->size; begin++)
  8. {
  9. pa->a[begin - 1] = pa->a[begin];
  10. }
  11. pa->size--;
  12. }

  测试pos下标删除正常  

指定pos下标删除实现后,我们可以对他进行复用,用它就可以实现头删和尾删功能

复用后的头删实现

  1. // 头删
  2. void SLPopFront(SL* pa)
  3. {
  4. SLErase(pa, 0);
  5. }

复用后的尾删实现

  1. // 尾删
  2. void SLPopBack(SL* pa)
  3. {
  4. SLErase(pa, pa->size - 1);
  5. }

 测试复用SLErase后的头删和尾删测试正常

总结

顺序表是最简单的一个数据结构,但是还需要注意一些细节,如:插入数据时容量检查,容量扩容中对realloc函数的掌握,删除数据size的判断头插和头删数据移动,在pos下标插入或删除范围的限制等细节

本篇文章到此结束,感谢你能看到这里,能帮到你,这是对我最大的鼓励,让我们朝着更好的未来加油吧!!!

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

闽ICP备14008679号