赞
踩
目录
前言
数据结构的常见线性表,分别是顺序表,链表,栈,队列
本篇给大家带来动态顺序表的实现和讲解
线性表是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表是用一段物理地址连续的存储单元,从头开始连续依次存储数据元素的线性结构,一般采用数据存储。
1.静态顺序表:使用定长数组存储
2.动态顺序表:使用动态开辟的数组存储
静态
动态
因为静态顺序表的长度是固定的,因此数组满了需要手动更改N,不然就如上图数据插入不成功,而动态顺序表就解决了这个问题。
动态顺序表当容量满了之后,会进行自动扩容
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
一般顺序表拥有这些接口
1. 初始化 SLInit
2. 销毁 SLDestroy
3. 容量检查 SLCheckCapacity
4.尾插 SLPushBack
5. 打印 SLPrint
6. 尾删 SLPopBack
7. 头插 SLPushFront
8. 头删 SLPopFront
9. 查找 SLFind
10. 指定下标插入 SLInsert
11. 指定下标插入 SLErase
顺序表动态存储结构
- // 动态顺序表
- typedef struct SeqList
- {
- SLDataType* a; // 动态开辟的数组的地址
- int size; // 有效数据的个数
- int capacity; // 容量空间的大小
- }SL;
- // 顺序表初始化
- void SLInit(SL* pa);
- void SLInit(SL* pa)
- {
- assert(pa); // 传入的地址不能是NULL
-
- pa->a = NULL;
- pa->size = pa->capacity = 0;
- }
注意我们需要地址传递,而不是值,因为我们值传递对实参没有任何改变,只是实参的临时拷贝,改变的是形参的内容。
通过地址传递初始化成功
- // 顺序表销毁
- void SLDestroy(SL *pa);
实现思路:
- // 顺序表打印
- void SLPrint(SL* pa);
遍历一遍顺序表,同时输出顺序表中的各个值
- // 打印
- void SLPrint(SL* pa)
- {
- assert(pa);
- for (int i = 0; i < pa->size; i++)
- {
- printf("%d ", pa->a[i]);
- }
- printf("\n");
- }
- // 顺序表容量检查
- void SLCheckCapacity(SL* pa);
- // 顺序表尾插
- void SLPushBack(SL* pa, SLDataType x);
顺序表的尾插特别简单,就是最后面增加一个数据
但是存放数据之前,顺序表必须是未满状态下,否则就无法存储
所以我们可以在尾删前,做一个容量检查的函数
容量检查函数就两个功能
具体实现如下:
- // 容量检查
- void SLCheckCapacity(SL* pa)
- {
- assert(pa);
-
- if (pa->size == pa->capacity)
- {
- int newCapacity = pa->capacity == 0 ? 4 : pa->capacity * 2;
- SLDataType* tmp = (SLDataType*)realloc(pa->a, newCapacity * sizeof(SLDataType));
- if (tmp == NULL)
- {
- printf("扩容失败");
- exit(-1);
- }
- pa->a = tmp;
- pa->capacity = newCapacity;
- }
- }
- 判断有效数据和容量大小是否相等,相等就满了
- 相等有会两种情况,1.就是刚初始化完 2.就是插入数据时
- 刚初始化完我们可以先给它一点空间,如果是插入时满了可以在原有容量的基础上扩容2倍,这样可以减少烦琐
- 这里可以用扩容函数realloc,它和malloc功能类似都是在堆(heap)上面开辟空间,但是它可以在扩容的基础上重新扩容,可以保存之前扩容的数据,malloc的话就是不能保存之前扩容的数据,可以看上图的测试,还有一个特性就是扩容的指针是NULL,就实现和malloc一样的功能,第二个参数单位是字节数,不是个数
- 如果扩容的指针等于NULL,NULL就是代表扩容失败了,失败就提示下然后异常退出,exit(-1):异常退出,exit(0):正常退出
- 扩容成功就把扩容好的指针赋值给指向数组的地址,容量大小更新为扩容好的容量大小
扩容函数实现完成后,我们开始实现尾插函数
先要考虑尾插的基本逻辑:
1. 需要容量检查,我们可以直接调容量检查函数
2. 容量够,我们就在size位置插入数据,然后size递增即可
因为数组下标从0开始,size位置是最后一个数据的后面
具体实现如下:
- // 尾插
- void SLPushBack(SL* pa, SLDataType x)
- {
- assert(pa);
-
- SLCheckCapacity(pa);
- pa->a[pa->size] = x;
- pa->size++;
- }
大家写完一个接口,可以测试下,不要一个劲的往后写,出题了问题,调试很麻烦
如上图,扩容了2次,第1次:初始化后扩容为4,第2次:容量满了扩容为8,尾插5次也成功插入
- // 顺序表尾删
- void SLPopBack(SL* pa);
尾删就是把size--吗?
尾删需要注意这几点:
第一个问题:
第二个问题:
需要判断,如果删除次数大于size个数,size的值就会为负数,就会导致越界,插入数据就会在开辟空间的前面插入,free这个开辟的空间,就会内存报错,而且你在越界的地方插入的数据,无法显示。
解决方法:只需要判断size的个数大于0,才能尾删,这里我用的是assert(断言),需要包含头文件assert.h,条件为真就正常,条件为假,就报错并且会在控制台打印出报错的位置。
具体实现如下:
- // 尾删
- void SLPopBack(SL* pa)
- {
- assert(pa);
- assert(pa->size > 0); // size个数判断
-
- pa->size--;
- }
测试尾删正常
- // 顺序表尾插
- void SLPushBack(SL* pa, SLDataType x);
头插就没像头插那么容易,因为顺序表是数组实现的,往往我们在数组第一个位置插入数据,比较麻烦
头插的思路:
1. 和尾插一样做容量检查
2. 需要把所有的数据都向后移动一位,就是下标0 - size-1,注意是最后一个数先移动,从后往前,如果从前开始就会造成数据覆盖
3. 把插入的数据放到下标0位置
具体实现如下:
- // 头插
- void SLPushFront(SL* pa, SLDataType x)
- {
- assert(pa);
-
- SLCheckCapacity(pa);
- for (int end = pa->size - 1; end >= 0; end--)
- {
- pa->a[end + 1] = pa->a[end];
- }
- pa->a[0] = x;
- pa->size++;
- }
扩容和头插都测试正常
- // 顺序表头删
- void SLPopFront(SL* pa);
头删和头插类似,都需要移动数据
头删的思路:
1. 删除前做判断,size必须大于0
2. 把下标1 - size-1的数向前移动,注意是第一个数据先移动,从前往后,如果从后面开始就 会造成数据覆盖
3. size--
具体实现如下:
- // 头删
- void SLPopFront(SL* pa) {
- assert(pa);
- assert(pa->size > 0);
-
- for (int begin = 1; begin < pa->size; begin++)
- {
- pa->a[begin - 1] = pa->a[begin];
- }
- pa->size--;
- }
测试头删正常
- // 顺序表查找数据
- int SLFind(SL* pa, SLDataType x);
查找顺序表中的某个数据,找到就返回这个数据下标,没有找到则就返回-1
只需要遍历一遍顺序表,判断下要找的数据和顺序表当前位置的值是否相等
具体实现如下:
- // 查找某个数,返回下标
- int SLFInd(SL* pa, SLDataType x)
- {
- assert(pa);
-
- for (int i = 0; i < pa->size; i++)
- {
- if (pa->a[i] == x)
- return i;
- }
- return -1;
- }
测试正常情况和未找到的情况
- // 顺序表指定下标插入
- void SLInsert(SL* pa, int pos, SLDataType x);
这里和头插类似,只不过移动区间变了,但是要注意pos不能随便插入,我们也需要判断
指定下标插入的思路:
具体实现如下:
- // 指定下标插入
- void SLInsert(SL* pa, int pos, SLDataType x)
- {
- assert(pa);
- assert(pos >= 0 && pos <= pa->size); // 插入的下标范围在0 - size
- SLCheckCapacity(pa);
-
- for (int end = pa->size - 1; end >= pos; end--)
- {
- pa->a[end + 1] = pa->a[end];
- }
- pa->a[pos] = x;
- pa->size++;
- }
测试pos下标插入正常
指定pos下标插入实现后,我们可以对他进行复用,用它就可以实现头插和尾插功能
复用后的头插实现
- // 头插
- void SLPushFront(SL* pa, SLDataType x)
- {
- SLInsert(pa, 0, x);
- }
复用后的尾插实现
- // 尾插
- void SLPushBack(SL* pa, SLDataType x)
- {
- SLInsert(pa, pa->size, x);
- }
测试复用SLInsert后的头插和尾插测试正常
- // 顺序表指定下标删除
- void SLErase(SL* pa, int pos);
这里和头删类似,只不过移动区间变了,但是要注意pos不能随便删除,我们也需要判断
指定下标插入的思路:
具体实现如下:
- // 指定下标删除
- void SLErase(SL* pa, int pos)
- {
- assert(pa);
- assert(pa->size > 0); // size大于0才可以删除
- assert(pos >= 0 && pos < pa->size); // 删除的范围下标0 - size-1
-
- for (int begin = pos + 1; begin < pa->size; begin++)
- {
- pa->a[begin - 1] = pa->a[begin];
- }
- pa->size--;
- }
测试pos下标删除正常
指定pos下标删除实现后,我们可以对他进行复用,用它就可以实现头删和尾删功能
复用后的头删实现
- // 头删
- void SLPopFront(SL* pa)
- {
- SLErase(pa, 0);
-
- }
复用后的尾删实现
- // 尾删
- void SLPopBack(SL* pa)
- {
- SLErase(pa, pa->size - 1);
- }
测试复用SLErase后的头删和尾删测试正常
顺序表是最简单的一个数据结构,但是还需要注意一些细节,如:插入数据时对容量检查,容量扩容中对realloc函数的掌握,删除数据的size的判断,头插和头删的数据移动,在pos下标插入或删除中范围的限制等细节
本篇文章到此结束,感谢你能看到这里,能帮到你,这是对我最大的鼓励,让我们朝着更好的未来加油吧!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。