【初阶数据结构】顺序表
目录
1. 线性表
2. 手撕顺序表
2.1 顺序表的分类
2.1.1 静态顺序表
2.1.2 动态顺序表
2.2 动态顺序表的实现
2.2.1 第一步:创建三个文件
2.2.2 第二步: 创建一个顺序表
2.2.3 第三步:初始化
2.2.4 第四步:尾插
2.2.5 第五步:头插
2.2.6 第六步:尾删
2.2.7 第七步:头删
2.2.8 第八步: 从指定数据位置插入数据
2.2.9 第九步:删除pos位置的数据
2.2.10 顺序表的销毁
2.2.11 全部代码
3. 顺序表算法题
3.1 算法题1:移除元素
3.2 算法题2:删除有序数组中的重复性
3.3 算法题3:合并两个有序数组
1. 线性表
在学习顺序表前,我们先来了解一下线性表。
线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构。常见的线性表:顺序表、链表、栈、队列、字符串……
线性表在逻辑上是线性结构,也就是说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上储存的时候,通常以数组和链式结构的形式储存。
在这里就有一个形象的例子:
我们在每周一升国旗的时候,学校会要求我们站成一条直线,也就是我们在逻辑上的一条直线,在物理层次,我们每个人与每个人之间是有一定的距离的,我们总不能手拉着手,你说对吧。我们简单的画一下图用来表明这个逻辑:
2. 手撕顺序表
概念:顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况项采用数组储存。
猛地一看,这和数组似乎没啥两样。那么他俩的区别是啥?
顺序表的底层结构是数组,对数组的封装,实现了常用的增删查改等接口
怎么说呢?就是增添了一点其他的元素。就拿我们玩过的游戏来举例:
害怕侵权,这里就不列出具体的游戏了,大家多担待。
2.1 顺序表的分类
2.1.1 静态顺序表
概念:使用定长数组储存元素。
typedef int SLDateType;
#define N 7
typedef struct SeqList
{SLDateType a[N]; // 定长数组int size; // 有效数据个数
}SL;
这里我们只是说明一下格式。
2.1.2 动态顺序表
多了一个成员,用来统计空间的大小。按需要申请空间。
typedef struct SeqList
{SLDataType* a;int size;int capacity;
}SL;
我们使用动态顺序表,其中capacity用来计算空间容量,size是有效数据的个数,这样呢就可以申请空间。相较于静态顺序表,结构复杂,但适用性提高,前者只能用于特定的场景中,后者使用的更加广泛。所以我们来实现一下动态顺序表。
2.2 动态顺序表的实现
我们使用多文件的形式来完成顺序表的创建,这样方便管理。就好比扫雷游戏的实现一样,大家可以去看看那篇文章-- 函数、数组实战:扫雷游戏的实现-CSDN博客
话不多说,我们开始一步一步实现动态顺序表。完整代码会放在小节末尾。
2.2.1 第一步:创建三个文件
我们使用SeqList.h写函数声明、以及各种库。使用SeqList.c编写顺序表的代码。使用test.c来测试代码的运行情况。
2.2.2 第二步: 创建一个顺序表
我们现在定义了一个顺序表,但是图中方框中,我们直接把类型定为了int,这样写的话,我们日后要是把int改为char,后面的代码都要修改,这样搞特麻烦,有聪明的同志说,我们可以使用一键替换啊,但是如果只是修改一部分呢?所以我们重命名一下:
// 定义一个动态的顺序表
typedef int SLTDataType;
typedef struct SeqList
{SLTDataType* arr; // 存储数据int size; // 有效数据的个数int capacity; // 空间大小
}SL;
顺便把结构体也重命名一下,不然每次调用都要写struct有点麻烦。
2.2.3 第三步:初始化
我们定义了一个动态的顺序表,那么现在来初始化。
// SeqList.h
// 初始化动态顺序表
void SLInit(SL sl);// SeqList.c
// 初始化动态顺序表
void SLInit(SL s)
{s.arr = NULL;s.size = s.capacity = 0;}// test.c
#include "SeqList.h"test_SLInit()
{SL s1;SLInit(s1);
}
int main()
{test_SLInit();return 0;
}
这是我们初次写完的初始化,我们先来测试一下:
编译错误,是什么原因呢?使用了未初始化的局部变量“s1”。
我们现在来想想为啥会出现这样的原因。之前我们在学习C语言的时候,我们就学过参数的调用有两种方式,传值 -- 传址 , 这里明显是传值调用,也就是说我们传了一个啥都没做的参数,所以会报错,因此这里我们应该使用传址调用,不改变原来参数。
// SeqList.h
void SLInit(SL* sl);// SeqList.c
// 初始化动态顺序表
void SLInit(SL* s)
{s->arr = NULL;s->size = s->capacity = 0;}// test.c
SLInit(&s1);
我们把内容修改为这样,编译通过。
2.2.4 第四步:尾插
我们实现了动态顺序表的初始化,接下来往顺序表里插入数据,我们首先从坑最多的尾插写起。
我们直接提供正确的代码,错误或细节的地方我们放在图中看。
// SeqList.h
// 尾插
void SLPushBcak(SL* ps, SLTDataType x);// SeqList.c
// 尾插
void SLPushBcak(SL* ps, SLTDataType x)
{int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;// 空间不够if (ps->size == ps->capacity){SLTDataType* temp = (SLTDataType*)realloc(ps->arr, newCapacity*sizeof(SLTDataType));if (temp == NULL){perror("realloc");exit(1); // 给任意非0值都行}ps->arr = temp;ps->capacity = newCapacity;}// 空间足够ps->arr[ps->size++] = x;
}// test.c
SLPushBcak(&s1, 1);
SLPushBcak(&s1, 2);
SLPushBcak(&s1, 3);
SLPushBcak(&s1, 4);
SLPushBcak(&s1, 5);
2.2.5 第五步:头插
在写头插的时候,我们先来画图,奠定思路。
我们现在来实现代码:
当我们先从空间足够的方法来写,当我们写到空间不够的时候,发现和尾插中增容的方式一模一样,所以我们把尾插的增容封装为一个函数。
// SeqList.c// 空间不够用的情况下增容
void CheckCapacity(SL* ps)
{int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;// 空间不够if (ps->size == ps->capacity){SLTDataType* temp = (SLTDataType*)realloc(ps->arr, newCapacity * sizeof(SLTDataType));if (temp == NULL){perror("realloc");exit(1); // 给任意非0值都行}ps->arr = temp;ps->capacity = newCapacity;}
}// 尾插
void SLPushBcak(SL* ps, SLTDataType x)
{assert(ps);// 空间不够CheckCapacity(ps);// 空间足够ps->arr[ps->size++] = x;
}// 头插
void SLPushFront(SL* ps, SLTDataType x)
{assert(ps);// 空间不够// 增容 -- 和尾插的增容同理// 把尾插中的增容封装为一个函数。CheckCapacity(ps);// 空间足够for (int i = ps->size; i > 0; i--){ps->arr[i] = ps->arr[i - 1];}ps->arr[0] = x;ps->size++;
}// SeqList.h// 头插
void SLPushFront(SL* ps, SLTDataType x);// test.c
#include "SeqList.h"void test_SLInit()
{SL s1;SLInit(&s1);/*SLPushBcak(&s1, 1);SLPushBcak(&s1, 2);SLPushBcak(&s1, 3);SLPushBcak(&s1, 4);SLPushBcak(&s1, 5);*/SLPushFront(&s1, 1);SLPushFront(&s1, 2);SLPushFront(&s1, 3);SLPushFront(&s1, 4);SLPushFront(&s1, 5);
}
这也就是代码看起来非常多,实际逻辑非常简单,就是for循环等的组合。
2.2.6 第六步:尾删
// 尾删
void SLPopBack(SL* ps)
{assert(ps && ps->size);ps->size--;
}
尾删就是这么简单,但是我们首先得让顺序表中有数据,以及ps不能是空指针,所以我们断言一下,需要包含头文件<assert.h>。同理头删也一样,接下来我们写头删的代码。
2.2.7 第七步:头删
我们仍然选择画图明确逻辑、
// 头删
void SLPopFront(SL* ps)
{assert(ps && ps->size);for (int i = 0; i < ps->size - 1; i++){ps->arr[i] = ps->arr[i + 1];}ps->size--;
}
同时我们使用SLPrint函数来打印顺序表。
2.2.8 第八步: 从指定数据位置插入数据
在插入数据之前我们需要知道指定位置,所以我们先来查找顺序表中有没有这个数据。
查找函数的代码如下:
// 查找顺序表中的数据
int SLFind(SL* ps, SLTDataType x)
{// 首先判断ps是否为空指针assert(ps);// 遍历顺序表查找数据。for (int i = 0; i < ps->size; i++){if (ps->arr[i] == x){return i;}}return -1;
}
我们有了查找,就可以明确数据在数据表中所处的位置,这样一来我们就可以在指定数据位置之前插入数据。还是老样子我们先来画图:
通过图中的逻辑我们可以写代码了。
// 在指定位置插入数据
void SLInsert(SL* ps, int pos, SLTDataType x)
{assert(ps);assert(pos >= 0 && pos < ps->size);// 判断空间是否足够CheckCapacity(ps);for (int i = ps->size; i > pos; i--){ps->arr[i] = ps->arr[i - 1];}ps->arr[pos] = x;ps->size++;
}
声明以及测试的代码就不在代码段中显示了,直接看结尾全部的代码。
2.2.9 第九步:删除pos位置的数据
有了上面画图辅助逻辑的基础,这里的逻辑非常简单。我们直接写代码。
// 删除pos位置的数据
void SLEras(SL* ps, int pos)
{assert(ps);assert(pos >= 0 && pos < ps->size);for (int i = pos; i < ps->size - 1; i++){ps->arr[i] = ps->arr[i + 1];}ps->size--;
}
以上就是我们实现的顺序表中的增删查改逻辑,现在我们要知道顺序表的销毁
2.2.10 顺序表的销毁
销毁就是释放空间,以及成员值归0。
我们直接看代码:
// 销毁顺序表
void SLDestory(SL* ps)
{if (ps->arr)free(ps->arr);ps->arr = NULL;ps->size = ps->capacity = 0;
}
2.2.11 全部代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// SeqList.h
// 定义一个动态的顺序表
typedef int SLTDataType;
typedef struct SeqList
{SLTDataType* arr; // 存储数据int size; // 有效数据的个数int capacity; // 空间大小
}SL;// 初始化动态顺序表
void SLInit(SL* sl);// 尾插
void SLPushBcak(SL* ps, SLTDataType x);// 头插
void SLPushFront(SL* ps, SLTDataType x);// 尾删
void SLPopBack(SL* ps);// 头删
void SLPopFront(SL* ps);// 打印顺序表
void SLPrint(SL* ps);// 查找顺序表中的数据
int SLFind(SL* ps, SLTDataType x);// 在指定位置插入数据
void SLInsert(SL* ps, int pos, SLTDataType x);// 删除pos位置的数据
void SLEras(SL* ps, int pos);// 销毁顺序表
void SLDestory(SL* ps);#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
// SeqList.c
// 初始化动态顺序表
void SLInit(SL* s)
{s->arr = NULL;s->size = s->capacity = 0;}// 销毁顺序表
void SLDestory(SL* ps)
{if (ps->arr)free(ps->arr);ps->arr = NULL;ps->size = ps->capacity = 0;
}// 空间不够用的情况下增容
void CheckCapacity(SL* ps)
{int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;// 空间不够if (ps->size == ps->capacity){SLTDataType* temp = (SLTDataType*)realloc(ps->arr, newCapacity * sizeof(SLTDataType));if (temp == NULL){perror("realloc");exit(1); // 给任意非0值都行}ps->arr = temp;ps->capacity = newCapacity;}
}// 尾插
void SLPushBcak(SL* ps, SLTDataType x)
{assert(ps);// 空间不够CheckCapacity(ps);// 空间足够ps->arr[ps->size++] = x;
}// 头插
void SLPushFront(SL* ps, SLTDataType x)
{assert(ps);// 空间不够// 增容 -- 和尾插的增容同理// 把尾插中的增容封装为一个函数。CheckCapacity(ps);// 空间足够for (int i = ps->size; i > 0; i--){ps->arr[i] = ps->arr[i - 1];}ps->arr[0] = x;ps->size++;
}// 尾删
void SLPopBack(SL* ps)
{assert(ps && ps->size);ps->size--;
}// 头删
void SLPopFront(SL* ps)
{assert(ps && ps->size);for (int i = 0; i < ps->size - 1; i++){ps->arr[i] = ps->arr[i + 1];}ps->size--;
}// 打印顺序表
void SLPrint(SL* ps)
{assert(ps);for (int i = 0; i < ps->size; i++){printf("%d ", ps->arr[i]);}printf("\n");
}// 查找顺序表中的数据
int SLFind(SL* ps, SLTDataType x)
{// 首先判断ps是否为空指针assert(ps);// 遍历顺序表查找数据。for (int i = 0; i < ps->size; i++){if (ps->arr[i] == x){return i;}}return -1;
}// 在指定位置插入数据
void SLInsert(SL* ps, int pos, SLTDataType x)
{assert(ps);assert(pos >= 0 && pos < ps->size);// 判断空间是否足够CheckCapacity(ps);for (int i = ps->size; i > pos; i--){ps->arr[i] = ps->arr[i - 1];}ps->arr[pos] = x;ps->size++;
}// 删除pos位置的数据
void SLEras(SL* ps, int pos)
{assert(ps);assert(pos >= 0 && pos < ps->size);for (int i = pos; i < ps->size - 1; i++){ps->arr[i] = ps->arr[i + 1];}ps->size--;
}#define _CRT_SECURE_NO_WARNINGS 1
// test.c
#include "SeqList.h"void test_SLInit()
{SL s1;SLInit(&s1);SLPushBcak(&s1, 1);SLPushBcak(&s1, 2);SLPushBcak(&s1, 3);SLPushBcak(&s1, 4);SLPushBcak(&s1, 5);//SLPushFront(&s1, 1);//SLPushFront(&s1, 2);//SLPushFront(&s1, 3);//SLPushFront(&s1, 4);//SLPushFront(&s1, 5);/*SLPopFront(&s1);SLPrint(&s1);SLPopFront(&s1);SLPrint(&s1);SLPopFront(&s1);SLPrint(&s1);SLPopFront(&s1);SLPrint(&s1);SLPopFront(&s1);SLPrint(&s1);*//*SLPopFront(&s1);SLPrint(&s1);*///// 查找//// 查找顺序表中的数据//int r = SLFind(&s1, 3);//if (r > 0)//{// printf("找到了\n");//}//else//{// printf("没找到\n");//}// 在指定位置插入数据int pos = SLFind(&s1, 2);SLInsert(&s1, pos, 100);SLPrint(&s1);SLInsert(&s1, pos, 200);SLPrint(&s1);SLInsert(&s1, pos, 300);SLPrint(&s1);SLInsert(&s1, pos, 400);SLPrint(&s1);}int main()
{test_SLInit();return 0;}
3. 顺序表算法题
我们经过前俩小节已经知道了了解了顺序表,现在我们来实战一下,顺序表算法题。
3.1 算法题1:移除元素
点链接就可以进入算法题页面。
我们拿到题目就可以审题构建思路了。还是直接使用画图来构建思路。
现在我们在题库中解题。
代码如下:
int removeElement(int* nums, int numsSize, int val)
{int s1 = 0, s2 = 0;while(s2 < numsSize){if (nums[s2] != val){nums[s1] = nums[s2];s1++;}s2++;}return s1;
}
3.2 算法题2:删除有序数组中的重复性
直接画图:
上代码:
int removeDuplicates(int* nums, int numsSize)
{int s1 = 0, s2 = 0;while (s2 < numsSize){if(nums[s1] != nums[s2]){s1++;nums[s1] = nums[s2];}s2++;}return s1 + 1;
}
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(l2 >= 0 && l1 >= 0){if (nums1[l1] > nums2[l2]){nums1[l3--] = nums1[l1--];}else{nums1[l3--] = nums2[l2--];}}while(l2 >= 0){nums1[l3--] = nums2[l2--];}
}