目录
1.前言
在介绍顺序表前,我们先讲下线性表,线性表简单来说,就是将n个具有一定特征的数据元素进行存储的数据结构,看起来很抽象,简单来说,字符串就是一个比较明显的线性表,为什么这么说?
字符串在c语言中的存储方式,是独立放在一个空间上,比如说
char a[10]="d2afwaga"; printf("%s",a); 在这里,printf其实接收到的只是字符串的第一个字符的地址,也就是d字符的地址, 然后(因为独立放在一个空间,字符串内的字符在物理内存上是连续的)以顺序的方式 (数组一样)不断读取字符,直到遇到\0才结束读取 我举这个例子,其实只是想说,字符串本身一般传参时就像数组一样,都是传递首元素地址 这样一来,其实数组也是一个线性表,但一般用到线性表的时候数组更多是作为其他类型线性表的 一部分组成(我会在其他线性表类型的讲解中讲到的)
线性表在逻辑上一定是连续的,但在物理空间上不一定是连续的,典型的是链表(逻辑连续)和顺序表(逻辑和物理都连续)
2.正文
2.1顺序表概念及结构
顺序表是指一个能够依次存储数据且在物理空间上也就是地址上是连续的一段空间。
一般我们采用数组(链接的形式也行,思路差不多,只要注意下用结构体指针的时候,为空等问题要注意一下)
顺序表(数组版)一般分为两类,静态和动态。
静态顺序表跟动态顺序表的区别主要是,一个是用定长数组一个是用动态数组。
静态顺序表
注意,这边的N,只是一个定值,我们在运行程序时是无法改变数组长度的。
size是记录数组中有效数组的长度(事实上数组数组的删除,我们是做不到的,我们可以规定的有效长度,达到另类的删除,因为数组空间开辟后,默认都是些随机值,我们把不要的数据也当做随机值处理即可)
SLDataType是一个别名,方便改变数据类型。
在实际运用中,我们存储数据大多数情况下,都是不确定要存储多少数据的,静态顺序表只支持提前确定要存储数据数量的情况下,所以我接下来主要是实现动态顺序表。
2.2动态顺序表实现
2.2.1动态顺序表头文件
#pragma once #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int SLDataType;//别名,方便改数据类型 typedef struct SeqList { SLDataType* a;//一个SLDataType类型的指针变量, //也是动态数组的指针 int size;//有效数据长度 int capacity;//动态数组长度,依靠改变这个变量, //再利用realloc改变空间大小,从而实现动态数组 }SL;//别名,加快代码实现速度 void SLCheckCapacity(SL* ps1);//改变动态数组大小用的函数, //因为频繁要用,所以我们直接单独写个函数,减少重复性代码 void SLInit(SL *ps1);//顺序表初始化 void SLDestroy(SL* ps1);//顺序表销毁 void SLPushBack(SL* ps1, SLDataType x);//顺序表尾插 void SLPushFront(SL* ps1, SLDataType x);//顺序表头插 void SLPopBack(SL* ps1);//顺序表尾删 void SLPopFront(SL* ps1);//顺序表头删 void SLPrint(SL* ps1);//顺序表打印 void SLInsert(SL* ps1, int pos, SLDataType x);//顺序表指定位置插入数据 void SLErase(SL* ps1, int pos);//顺序表指定位置删除数据 int SLFind(SL* ps1, SLDataType x);//寻找指定数据的位置
2.2.2动态顺序表源文(重点!)
2.2.2.1顺序表初始化
void SLInit(SL* ps1) { assert(ps1); ps1->a= NULL; ps1->size = 0; ps1->capacity = 0; } 顺序表的初始化,传参时用指针变量接受,实现改变真正的数组数据等。 assert语句是用来断言的,如果参数表达式为假, 运行时就会报错(防止疯狂死循环),实际使用时也是有助于以免机子死机 初始化时,a指针赋予NULL,size和capacity也都赋予0, 因为此时没有数据,也没必要开辟动态数组
2.2.2.2顺序表的销毁
void SLDestroy(SL* ps1) { assert(ps1);//断言 if (ps1->a != NULL) { free(ps1->a); ps1->a = NULL; ps1->size = 0; ps1->capacity = 0;// } } 传参采用一级指针 如果传参时已经是空,就不用了,避免引用野指针。 用free释放动态数组空间 释放后,置空,防止后续引用野指针 跟前言说的一样,删除数组数据,其实只能通过(简称不理他)的方式 所以只要把size=0即可。 再把capacity置为空,没什么用,就说明下动态数组此时理论上大小应该为空
2.2.2.3顺序表尾插
void SLPushBack(SL* ps1, SLDataType x) { assert(ps1); SLCheckCapacity(ps1); ps1->a[ps1->size] = x; ps1->size++; } 顺序表的尾插,传参采用一级指针和一个SLDataType类型数据 assert断言,SLCheckCapacity是用来扩容的(改变动态数组大小,这里还没实现 先别管,理解干什么就可以,待会会讲到的) 插入,我们要注意,数组空间是否够用,不管三七二十一,先调用函数一次 为什么这样,待会讲扩容函数的时候会说的。 接下来,就是把x值赋给动态数组size下标的空间上,(size是有效数据长度 因为数组下标是从0开始,所以size下标位置正好是有效数据的下一个无效位置) 记得size++
2.2.2.4顺序表打印
void SLPrint(SL* ps1) { assert(ps1); for (int i = 0; i < ps1->size; ++i) { printf("%d ", ps1->a[i]); } printf("\n"); } 方便调试用的,因为我们要快速测试代码是否正确, 本质上就是for循环打印数组内容罢了
2.2.2.5顺序表扩容(重点!)
void SLCheckCapacity(SL* ps1) { assert(ps1); if (ps1->size == ps1->capacity) { int newCapacity = ps1->capacity == 0 ? 4 : ps1->capacity * 2; SLDataType* tmp = (SLDataType*)realloc(ps1->a, sizeof(SLDataType) * newCapacity); if (tmp == NULL) { perror("realloc fail"); return; } ps1->a = tmp; ps1->capacity = newCapacity; } } 传参采用一级指针,然后是正常的断言,防止传空指针(传了空指针 后面如果引用数组,会造成越界访问,内容访问错误) 接下来判断size是否等于capacity,size是有效数组数量 ,size==capacity说明此时只有一个空位了,为了下次数据的增加,需要扩容 (这也是为什么,尾插的时候要直接甩这个函数上去,其他插入也是一样,都是因为 这里会自动判断空间是否需要扩容) 那么问题来了,扩容一次扩多少呢,其实扩多了会浪费,扩少了会造成频繁 申请动态空间,所以最终,我们默认采用2倍扩容。 接下来定义一个newcapacity,用三目表达式,如果此时空间为空, 直接给4个SLDataType大小字节,不为空,则*2,也就是扩容2倍 (这也是为什么,我们最初初始化不用赋予空间大小,一切操作都放在这里) 接下来realloc动态申请空间(当指针为空时,效果等同于malloc),大小用newcapacity 因为动态内存函数都有开辟失败的可能,所以这里要用一个判断(开辟失败,只会返回 空指针),为真就结束函数 为假则直接把地址传给动态数组的指针,让指针指向一个有效的数组空间 记得更新数组大小capacity
2.2.2.6顺序表头插
void SLPushFront(SL* ps1, SLDataType x) { assert(ps1); SLCheckCapacity(ps1); int end = ps1->size - 1; while (end >= 0) { ps1->a[end + 1] = ps1->a[end]; --end; } ps1->a[0] = x; ps1->size++; } 其他跟尾插一样,主要是我们要额外一个循坏,把整个数组往后挪 这也是顺序表的劣势,尾插很方便,头插很麻烦 end是最后一个有效数据的下标,因为是往后挪, while条件是end>=0,然后把end位置的数据放在end+1上,再end-- 循坏结束后,把要插入的数据放在0下标上,size记得++即可。
2.2.2.7顺序表尾删
void SLPopBack(SL* ps1) { assert(ps1); assert(ps1->size); ps1->size--; } 尾插,顺序表对尾部的操作是很快的,体现在 代码上就是,尾删只需要size--即可。 (size是有效数据数量,数组删除是以一种自欺欺人的方式删除的) 还要注意的是断言部分,不仅要断言是否是空指针,还要判断当前数组 空间是否还有有效数据。
2.2.2.8顺序表头删
void SLPopFront(SL* ps1) { assert(ps1); assert(ps1->size > 0); int begin = 1; while (begin < ps1->size) { ps1->a[begin - 1] = ps1->a[begin]; ++begin; } ps1->size--; } 头删,注意断言两处,空指针和空数组 将begin位置的数据覆盖到begin-1位置,实现向前挪 记得begin++ 循坏结束,即可size--
2.2.2.9顺序表任意位置插入
void SLInsert(SL* ps1, int pos, SLDataType x) { assert(ps1); assert(pos>=0 && pos<=ps1->size); SLCheckCapacity(ps1); int end = ps1->size - 1; while (end >= pos) { ps1->a[end + 1] = ps1->a[end]; --end; } ps1->a[pos] = x; ps1->size++; } pos是插入的位置 断言2处,空指针和pos是否合法 接下来正常扩容判断 把直到pos的数据都往后挪 然后把值放入pos下标位置 记得size++
2.2.2.10顺序表任意位置删除
void SLErase(SL* ps1, int pos) { assert(ps1); assert(pos >= 0 && pos < ps1->size); int begin = pos + 1; while (begin < ps1->size) { ps1->a[begin - 1] = ps1->a[begin]; begin++; } ps1->size--; } 断言两处,空指针和pos位置是否合法 把begin位置数据覆盖到begin-1,也就是pos位置 然后begin++,不停覆盖,最后size--,实现删除指定位置数据
2.2.2.11顺序表位置寻找
int SLFind(SL* ps1, SLDataType x) { assert(ps1); for (int i = 0; i < ps1->size; i++) { if (ps1->a[i] == x) { return i; } } return - 1; } 遍历数组,找到值,返回下标,没找到返回-1
2.3.顺序表的例题
2.3.1例1
这道题,其实不是顺序表,但有助于理解顺序表
int removeElement(int* nums, int numsSize, int val){ int src=0,dst=0; while(src<numsSize) { if(nums[src]==val) { src++; } else { nums[dst]=nums[src]; src++; dst++; } } return dst; } src和dst都是指向下标 循坏条件src<numsSize,保证下标不越界 如果src下标位置数据等于val,src指向下一个下标 如果不等于,则把src下标数据覆盖给dst 最初是自己覆盖自己,后面遇到val值, 这时候dst才是真正的有效数据长度
2.3.2例2
26. 删除有序数组中的重复项 - 力扣(LeetCode)
int removeDuplicates(int* nums, int numsSize) { str = 1, r = 1, a = 0; while(r<numsSize) { if (nums[r] == nums[a]) { r++; } else { nums[str] = nums[r]; str++; a++; } } return str; } str是最后一个检查的数据的下一个数据,用来被覆盖的 a指向的下标都是已经确定过了,不是重复 遇到重复的,r++,不重复,再覆盖到str上,str和a再++
2.3.3例3
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){ int l1=m-1;int l2=n-1; int l3=m+n-1; while(l1>=0 && l2>=0) { if(nums1[l1]>nums1[l2]) { nums1[l3--]=nums1[l1--]; } else { nums1[l3--]=nums1[l2--]; } } while(l2>=0) { nums1[l3--]=nums1[l2--]; } } 两个数组数据比较,大的放入nums1数组的最后一个,接着往下走 大的放后面,如果l1先变空,则直接把nums2剩下的放入nums1, l2先变空,则已经完成了合并,直接结束即可
2.4顺便表的特点
指定位置插入和头插,指定位置删除,头删,每次都要遍历一遍数组,所以时间复杂度都是O(N)
扩容的时候,因为动态内存函数的特性,如果遇到连续空间不够用了,会拷贝数据到一个新的位置,这时候是有消耗的
扩容的时候,每次扩容2倍,那么如果只是要加1个数据,多的都是浪费的,所以我们需要链表,链表不会出现空间浪费的情况,链表请看我链表的文章(写这篇的时候还没写,你们看到的时候应该有了)