2. 顺序表和链表

文章目录
- [2. 顺序表和链表](#2. 顺序表和链表)
-
- [1. 线性表](#1. 线性表)
- [2. 顺序表](#2. 顺序表)
-
- [2.1 概念与结构](#2.1 概念与结构)
- [2.2 分类](#2.2 分类)
-
- [2.2.1 静态顺序表](#2.2.1 静态顺序表)
- [2.3 动态顺序表](#2.3 动态顺序表)
-
- [2.3.1 动态顺序表的实现](#2.3.1 动态顺序表的实现)
- [2.3 顺序表算法题](#2.3 顺序表算法题)
-
- [2.3.1 移除元素](#2.3.1 移除元素)
- [2.3.2 删除有序数组中的重复项](#2.3.2 删除有序数组中的重复项)
- [2.3.3 合并两个有序数组](#2.3.3 合并两个有序数组)
- [2.4 顺序表问题与思考](#2.4 顺序表问题与思考)
1. 线性表
线性表(linearlist)是n个具有相同特性的数据元素 的有限序列。线性表是⼀种在实际中⼴泛使⽤的 数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上 是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的 ,线性表在物理上存储 时,通常以数组 和链式结构的形式存储。
2. 顺序表
2.1 概念与结构
概念:顺序表是⽤⼀段物理地址连续的存储单元依次存储数据元素的线性结构,⼀般情况下采⽤数组 存储。

顺序表和数组的区别?
顺序表的底层结构是数组,对数组的封装,实现了常⽤的增删改查等接⼝

2.2 分类
2.2.1 静态顺序表
- 概念:使⽤定⻓数组存储元素

静态顺序表代码:
//静态顺序表
typedef int SLDataType;
#define N 7
typedef struct StationnarySeqList
{
SLDateType a[N]; //定长数组
int size; //顺序表合法数据大小
}sl;
2.3 动态顺序表

2.3.1 动态顺序表的实现
SeqList.h
#include <stdio.h> // printf、perror
#include <stdlib.h> // malloc、realloc、free
#include <assert.h> // assert 断言
//动态顺序表------按需申请
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a; //int类型的指针,用来动态调整顺序表大小
int size; //当前存储数据的个数
int capacity; //顺序表空间大小
}SL;
//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);
//扩容
void SLCheckCapacity(SL* ps);
//头部插⼊删除
//尾部插⼊删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
//指定位置之前插⼊
//删除数据
void SLInsert(SL* ps, int pos, SLDataType x);
void SLDelete(SL* ps, int pos);
//按值查找(顺序查找)
//在顺序表SL中查找第一关值等于e的元素,并返回其位序。即第几个元素
int LocateElem(SL* ps, SLDataType e);
SeqList.c
#include "SeqList.h";
//初始化
void SLInit(SL* ps)
{
//为什么咱们这里要传顺序表SL的地址?
//因为我们要初始化SL
//在顺序表的结构体定义里面
// 我们不能对里面的数据进行初始化
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
//销毁
void SLDestroy(SL* ps)
{
//释放堆上申请的空间
free(ps->a);
//指针置空,避免野指针
ps->a = NULL;
//顺序表数据清零
ps->size = 0;
ps->capacity = 0;
}
//扩容
void SLCheckCapacity(SL* ps)
{
//满了扩容
if (ps->size == ps->capacity)
{
//计算新容量:刚开始为0就给4,否则翻倍
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
//扩容核心函数
SLDataType* temp = (SLDataType*)realloc(ps->a, newcapacity * sizeof(SLDataType));
//防止扩容失败
if (temp == NULL)
{
perror("realloc failed");
return;
}
//扩容成功,更新指针和容量
ps->a = temp;
ps->capacity = newcapacity;
}
}
//打印顺序表
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
//尾插
void SLPushBack(SL* ps, SLDataType x)
{
//断言:谨防传了一个NULL进来的这种低级错误。
assert(ps);
// 1. 先检查扩容
SLCheckCapacity(ps);
//2. 尾部插入:size位置就是最后一个数据的下一位
ps->a[ps->size] = x;
// 3. 有效数据+1
ps->size++;
}
//尾删
void SLPopBack(SL* ps)
{
assert(ps);
//断言:谨防传了一个NULL进来的这种低级错误。
assert(ps->size > 0);
//断言:当前顺序表内必须有存储的数据,这样我们才能删除
ps->size--;
// 直接让有效数据-1,逻辑删除(数据还在,但访问不到)
}
//头插
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
//核心:数据全部往后移动一位,给头部移位置、
for (int i = ps->size; i > 0; i--)
{
ps->a[i] = ps->a[i - 1];
}
//0号位置插入
ps->a[0] = x;
ps->size++;
}
//头删
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
//跟尾插反着来
//核心: 数据全部往前移动一位,覆盖掉第一个数据
for (int i = 0; i < ps->size; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
//指定位置插入
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
// 检查pos合法性:0 <= pos <= size
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
// 从pos + 1位置,数据全部往后移动一位
for (int i = ps->size; i > pos; i--)
{
ps->a[i] = ps->a[i - 1];
}
ps->a[pos] = x;
ps->size++;
}
//指定位置删除
void SLDelete(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
// 数据往前挪一位,覆盖pos位置的数据
for (int i = pos; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
//按值查找(顺序查找)
//在顺序表SL中查找第一关值等于e的元素,并返回其位序。即第几个元素
int LocateElem(SL* ps, SLDataType e)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == e)
return i + 1;
}
return -1;
}
//测试
void test()
{
SL sl; // 结构体变量:在栈上
SLInit(&sl); // 初始化
// 测试尾插
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
printf("尾插1 2 3:");
SLPrint(&sl);
// 测试头插
SLPushFront(&sl, 0);
printf("头插0:");
SLPrint(&sl);
// 指定位置插入(在下标2插入99)
SLInsert(&sl, 2, 99);
printf("下标2插入99:");
SLPrint(&sl);
// 尾删
SLPopBack(&sl);
printf("尾删:");
SLPrint(&sl);
// 头删
SLPopFront(&sl);
printf("头删:");
SLPrint(&sl);
// 指定位置删除(删除下标1)
SLDelete(&sl, 1);
printf("删除下标1:");
SLPrint(&sl);
//查找顺序表内第一个值等于x的元素的序位
int x = 2;
printf("查找顺序表内第一个值等于x的元素的序位\n");
printf("x等于 %d \n", x);
int ret = LocateElem(&sl, x);
printf("第一个值等于x的元素的序位为 %d \n", ret);
// 销毁(释放堆内存)
SLDestroy(&sl);
}
main.c
#include "SeqList.h";
int main()
{
test();
return 0;
}
输出结果:

2.3 顺序表算法题
2.3.1 移除元素


对于本题我们可以采用快慢双指针算法思想,fast先走,slow后走。for循环内,只要numsfast 为 val,我们就跳过,来到下一个元素,设为A。若 A != val,即numsfast != val,那么我们就执行覆盖操作。用 numsslow = numsfast 可以实现 覆盖操作,注意覆盖完后slow要往后走一步,即slow++。
代码如下:

2.3.2 删除有序数组中的重复项
删除有序数组中的重复项(https://leetcode.cn/problems/remove-duplicates-from-sorted-array/description/


这道题和刚才那道题的覆盖思路其实是一样的,都是运用了双指针算法思想。唯一的区别是,刚才那道题给定了val,也就是说数组里最多只要删一种数值类型的元素。
但是,这道题它并没有给定val,意味着,数组中每个元素都有可能有重复的元素。因此,我们在覆盖细节上会有所差异。
上一道题里,数组中的第一个元素可能就是val,所以我们选择让fast从0开始自增。
而这一道题里,要删删除重复的元素,则应该从第二个元素开始自增,即fast初始为1。(其实fast从0开始自增也是可以的,只是我剪枝了一下。但好像有点做无用功,哈哈哈哈哈哈哈哈,是我多嘴了,哈哈哈哈哈哈)
好了,现在让我们看for循环里面的覆盖逻辑。依据上一题:
if( var != nums[fast])
{
nums[slow++] = nums[fast];
}
我们可以将if条件换成:nums[slow] != nums[fast]
然后根据测试用例进行算法推演:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
若继续采用上题算法 nums[slow++] = nums[fast]; ,咱们进行枚举:
- slow=0, fast=1; nums0 == nums1, fast++
- slow=0, fast=2; nums0 != nums2, 进行覆盖
nums0被修改为2,随后slow = 1, fast = 3,循环结束。
- 最后返回数组长度slow + 1 == 2,程序输出为2,1。答案错误。
因此我们需要更换细节,其实也就是把numsslow++ = numsfast; 变成nums++slow = numsfast;
这样保证了数组里最先出现的那种数值类型的重复元素里的第一个元素不会被覆盖。比如说1,1,1,2,根据这个算法,我们可以保证重复元素数值为1的这种类型的元素集合,第一个为1的元素,不会被覆盖,即跳过。
枚举过程如下:
- slow = 0, fast = 1; nums0 == nums1, fast++
- slow =0, fast = 2; nums0 == nums2, fast++
- slow = 0, fast = 3; nusm0 != nums3,先slow++,跳过nums0,得到slow == 1,再执行 numsslow = numsfast,进行覆盖。最后fast++,fast == 4,循环结束。
- 最终返回结果数组长度为 slow + 1 == 2,结果数组输出为1,2。答案正确
代码实现如下:

2.3.3 合并两个有序数组


这道题如果用c++的话,那真的有点开挂了,so easy。以下是c++版本
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
vector<int> res;
// 只取前 m 个
for (int i = 0; i < m; ++i) {
res.push_back(nums1[i]);
}
for (int x : nums2) {
res.push_back(x);
}
sort(res.begin(), res.end());
// 写回 nums1
nums1 = res;
}
};
但是为了检验咱们的代码功底,本着精益求精的态度,咱们直接用c语言,大力出奇迹
c语言版本:
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
// 三个指针:分别指向 nums1有效末尾、nums2末尾、合并后末尾
int i = m - 1;
int j = n - 1;
int k = m + n - 1;
// 从后往前放更大的数
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}
// 如果 nums1 还有剩余,直接覆盖到前面
while (i >= 0) {
nums1[k--] = nums1[i--];
}
// 如果 nums2 还有剩余,直接覆盖到前面
while (j >= 0) {
nums1[k--] = nums2[j--];
}
}
2.4 顺序表问题与思考
- 中间/头部的插⼊删除,时间复杂度为O(N)
- 增容需要申请新空间,拷⻉数据,释放旧空间。会有不⼩的消耗。
- 增容⼀般是呈2倍的增⻓,势必会有⼀定的空间浪费。例如当前容量为100,满了以后增容到200, 我们 再继续插⼊了5个数据,后⾯没有数据插⼊了,那么就浪费了95个数据空间。
- 思 考:如何解决以上问题呢?
小编最近两个月系统的学习了操作系统理论这本书,进行了大量六级英语真题练习,同时学习了mysql,git工具使用等,所以断更许久,还请见谅。
又是美好的一天,各位观众姥爷们,若有什么不懂的地方,欢迎在评论区留言哦,我会给出相应解释的。如有什么疏忽的细节,请点出,我会及时纠正。感谢观看。