一、顺序表
- 线性表:在逻辑上是线性结构,也就是说是连续的一条直线。但在物理结构上并不一定是连续的。线性表在物理上存储时,通常以数组和链式结构存储。
- 常见的线性表:顺序表、链表、栈、 队列、字符串……
- 顺序表就是顺序存储(物理结构)的线性表(逻辑结构);
- 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。(顺序表就是数组,但是在数组的基础上,它还要求数据是从头开始连续存储的,不能跳跃间隔)
二、顺序表分静态和动态:
静态顺序表:使用定长数组存储
动态顺序表:使用动态开辟的数组存储
- 动态顺序表会根据当前的需求进行扩容,因此将arr定义为指针
- size记录顺序表中实际存在的元素个数
- capacity记录顺序表的容量,当size == capacity时需要进行扩容
三、快速复习
- 结构定义:
结构体定义顺序表:指针,大小,容量;
-
相关函数:
-
构造析构:
- 初始化:分配初始空间,设置大小容量。
- 销毁:释放动态内存,大小容量清零。
-
增删查改:
- 遍历:普通循环遍历
- 判满扩容:判满,计算新容量,realloc扩容。注意这里要用临时指针接收realloc的返回地址,防止内存分配失败丢失数据。
- 头插:判满扩容,向后挪动数据(从后往前挪),首元素赋值,size++
- 尾插:判满扩容,尾元素赋值,size++
- 任意位置插:检查pos([0,size]),判满扩容,向后挪动pos及以后的数据(从后往前挪),pos赋值,size++
- 头删:判空,向前挪动首元素后的数据(覆盖首元素,从前往后挪),size–
- 尾删:判空,size–
- 任意位置删:检查pos([0,size)),判空,向前挪动pos后的数据(覆盖pos,从前往后挪),size–
- 遍历查找:无需多言
- 二分查找:定义左右指针,循环内(left <= right)取中判断与目标值的大小,移动左右指针(left = mid+1; right = mid-1;)。注意二分查找的使用条件,和边界问题。
-
判空判满:
-
判空:return size == 0;
-
判满:return size == capacity;
-
大小:return size;
-
-
四、基本操作
1.结构的声明及定义
typedef int SeqListData;
typedef struct SeqList{
SeqListData *arr;
int size;
int capacity;
}SeqList;
SeqList sl;
SeqListInit(&sl);
2.插入元素
头插
void SeqListPushFront(struct SeqList *psl, SLDataType val){
SeqListChackCapacity(psl);
int end = psl->size - 1;
while (end >= 0)
{
psl->arr[end + 1] = psl->arr[end];
end--;
}
psl->arr[0] = val;
psl->size++;
}
尾插
void SeqListPushBack(struct SeqList *psl, SLDataType val){
SeqListChackCapacity(psl);
psl->arr[psl->size] = val;
psl->size++;
}
任意位置插入
void SeqListInsert(struct SeqList *psl, SLDataType val, int pos){
assert(pos >= 0);
assert(pos <= psl->size);
SeqListChackCapacity(psl);
int end = psl->size - 1;
while (end >= pos)
{
psl->arr[end + 1] = psl->arr[end];
end--;
}
psl->arr[pos] = val;
psl->size++;
}
注意:插入位置pos的取值范围是[0,size],注意size位置可取。
3.删除元素
头删
void SeqListPopFront(struct SeqList *psl){
assert(psl->size != 0);
int tmp = 1;
while (tmp <= psl->size-1)
{
psl->arr[tmp - 1] = psl->arr[tmp];
tmp++;
}
psl->size--;
}
尾删
void SeqListPopBack(struct SeqList *psl){
assert(psl->size != 0);
psl->size--;
}
任意位置删除
void SeqListErase(struct SeqList *psl, int pos){
assert(pos >= 0);
assert(pos < psl->size);
int tmp = pos + 1;
while (tmp <= psl->size-1)
{
psl->arr[tmp - 1] = psl->arr[tmp];
tmp++;
}
psl->size--;
}
注意:
- 删除位置pos的取值范围是[0,size)(返回条件与之相反),注意size位置不可取。
- SeqListErase不需要另写代码判断psl->size是否为0,因为在判断pos的合法性时已将这种情况考虑在内。
4.查找
遍历查找
int SeqListFind(struct SeqList *psl, SLDataType val){
int i = 0;
for ( i = 0; i < psl->size; i++)
{
if (psl->arr[i] == val)
{
return i;
}
}
return -1;
}
二分查找
int BinarySearch(int* nums, int numsSize, int target){
int left = 0;
int right = numsSize-1;
int mid;
while(left<=right)
{
mid = (left+right)/2;
if(nums[mid] > target){
right = mid-1;
}
else if(nums[mid] < target){
left = mid+1;
}
else{
return mid;
}
}
return -1;
}
二分查找的使用前提:
- 数组为有序数组
- 数组中无重复元素(否则返回的元素下标可能不是唯一的)
易错点:边界问题
大家写二分法经常写乱,主要是因为对区间的定义没有想清楚,区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
- 明确区间的定义是[left,right](左闭右闭)还是[left,right)(左闭右开)。
- 区间的定义这就决定了二分法的代码应该如何写,如果定义target在[left, right]区间,所以有如下三点:
- right = numsSize-1;
- while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=
- if (nums[middle] > target) right = middle - 1(left = middle+1),因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1(这里如果错写成right = middle,当right == left时会造成死循环)
- 如果定义target在[left, right)区间,所以有如下三点:
- right = numsSize;
- while (left < right) 要使用 <,因为left == right在区间中是没有意义的。
- if (nums[middle] > target) right = middle(left = middle+1),因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle]
5.其他操作
初始化
//初始化
void SeqListInit(struct SeqList *psl){
psl->arr = NULL;
psl->size = 0;
psl->capacity = 0;
}
打印
//打印
void SeqListPrint(struct SeqList *psl){
int i = 0;
for ( i = 0; i < psl->size; i++)
{
printf("%d ", psl->arr[i]);
}
printf("\n");
}
销毁
//销毁
void SeqListDestroy(struct SeqList *psl){
free(psl->arr);
psl->arr = NULL;
psl->size = 0;
psl->capacity = 0;
}
检查是否需要扩容
//检查是否需要扩容
void ChackCapacity(struct SeqList *psl){
if (psl->size == psl->capacity)
{
int newcapacity = ((psl->capacity == 0) ? 5 : psl->capacity * 2);
SLDataType *tmp = (SLDataType *)realloc(psl->arr, newcapacity*sizeof(SLDataType));
if (tmp == NULL)
{
perror("ChackCapacity");
exit(1);
}
psl->arr = tmp;
psl->capacity = newcapacity;
}
}
四、易错点
- ChackCapacity();检查是否需要扩容时,要定义新指针接受重新分配的内存空间。(malloc/realloc/free 的使用要点)
- 重新分配内存空间后记得修改顺序表容量的大小(capacity);
- 无论是那种插入元素的方式,都要在正式插入之前进行容量检查,查看是否需要扩容。
- 无论是那种删除元素的方式,都要在正式删除之前判断顺序表是否为空(size==0?)。
- SeqListErase();在指定位置删除元素时。不需要另写代码判断顺序表是否为空(size==0?),因为在判断pos的合法性时已将这种情况考虑在内。
- 任意位置插入删除元素时,要提前判断pos的合法性(上文提到)。