【数据结构】顺序表0基础知识讲解 + 实战演练
一、顺序表的概念与结构
概念:顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。
顺序表和数组的区别?
顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接口(顺序表是用数组来实现的),顺序表在逻辑结构和物理结构上均为线性的。
下面为大家举一个生活中的例子来为大家解释数组与顺序表的关系:
二、顺序表的分类
2.1、静态顺序表
概念:使用定长数组存储元素
注:表中有一处错误,宏定义那里应该改为 #define N 6
静态顺序表缺陷:空间给少了不够用,给多了造成空间浪费
2.2、动态顺序表
三、动态顺序表的功能以及实现(重点)
我们在实现顺序表的时候采用我们之前写扫雷游戏的类似思想,将一个很长的代码拆分为不同功能,写入头文件 SeqList.h 来定义函数,SeqList.c 来实现函数的基本功能,test.c 来进行顺序表的功能的测试。
3.1、SeqList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>#define _CRT_SECURE_NO_WARNINGS
//静态顺序表:空间给小了不够用,空间给大了会造成浪费
//动态顺序表:可以动态扩容//定义动态顺序表的结构
typedef int SLTDataType;
typedef struct SeqList
{SLTDataType* arr; //存储数据int size; //有效数据个数int capacity; //空间大小
}SL;void SLPrint(SL* ps);//顺序表的打印
void SLInit(SL* ps);//顺序表初始化
void SLDestroy(SL* ps);//顺序表销毁//尾插 -- PushBack
//时间复杂度0(1)
void SLPushBack(SL* ps, SLTDataType x);//头插
//时间复杂度0(N)
void SLPushFront(SL* ps, SLTDataType x);//尾删
//时间复杂度0(1)
void SLPopBack(SL* ps);//头删
//时间复杂度O(N)
void SLPopFront(SL* ps);//查找
int SLFind(SL* ps, SLTDataType x);//指定位置之前插入
//时间复杂度0(N)
void SLInsert(SL* ps, int pos, SLTDataType x);//指定位置之后插入
//时间复杂度O(1)
void SLInsertAfter(SL* ps, int pos, SLTDataType x);//删除pos位置的数据
void SLErase(SL* ps, int pos);//改变pos位置的数据
void SLModify(SL* ps, int pos, SLTDataType x);
3.2、SeqList.c
#include"SeqList.h"//顺序表初始化
void SLInit(SL* ps)
{ps->arr = NULL;ps->size = ps->capacity = 0;
}//顺序表打印
void SLPrint(SL* ps)
{for (int i = 0; i < ps->size; i++){printf("%d ", ps->arr[i]);}printf("\n");
}//顺序表销毁
void SLDestroy(SL* ps)
{if (ps->arr)free(ps->arr);//对我们realloc的空间进行释放ps->arr = NULL;ps->size = ps->capacity = 0;
}//增容
void SLCheckCapacity(SL* ps)
{//空间不够(size == capacity)if (ps->size == ps->capacity){int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;//增容:我们一般成倍数增加//(能够有效降低增容次数,存在浪费空间,但不会太对)//(频繁的增容会使代码效率低下,联想realloc工作原理)SLTDataType* tmp = (SLTDataType*)realloc(ps->arr, newcapacity * sizeof(SLTDataType));if (tmp == NULL){perror("realloc fail!");exit(1);}ps->arr = tmp;ps->capacity = newcapacity;}
}//尾插
void SLPushBack(SL* ps, SLTDataType x)
{//空间不够SLCheckCapacity(ps);//空间足够ps->arr[ps->size] = x;ps->size++;//插入了一个有效数字,有效数据个数就要+1
}//头插
void SLPushFront(SL* ps, SLTDataType x)
{//处理ps为NULL的情况if (ps == NULL)return;//用assert断言处理他会报错告诉你错误的地方assert(ps != NULL);//等价于assert(ps)//空间不够SLCheckCapacity(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);assert(ps->size);//size代表的是有效个数,数组下标是从0开始的ps->size--;
}//头删 -- 删除的前提是顺序表不能为空
void SLPopFront(SL* ps)
{assert(ps);assert(ps->size);//头删我们删除的是下标为0位置的数据//我们将i+1位置给i,然后i++//数据整体向前挪动一位for (int i = 0; i < ps->size - 1; i++){ps->arr[i] = ps->arr[i + 1];}ps->size--;
}//查找 -- 在顺序表中查找值为x的数据
int SLFind(SL* ps, SLTDataType x)
{assert(ps);for (int i = 0; i < ps->size; i++){if (ps->arr[i] == x){//找到了return 1;}}//未找到return -1;
}//在指定位置之前插入
void SLInsert(SL* ps, int pos, SLTDataType x)
{assert(ps);//0 <= pos < ps->sizeassert(pos >= 0 && pos < ps->size);//判断空间是否足够SLCheckCapacity(ps);//我们想要插入的位置在pos前面//pos及之后数据向后挪动一位for (int i = ps->size; i > pos; i--){ps->arr[i] = ps->arr[i - 1];}//接着插入你想要插入的数字ps->arr[pos] = x;ps->size++;
}//在指定位置之后插入
void SLInsertAfter(SL* ps, int pos, SLTDataType x)
{assert(ps);//0 <= pos < ps->sizeassert(pos >= 0 && pos < ps->size);//判断空间是否足够SLCheckCapacity(ps);//我们想要插入的位置在pos后面//pos之后的数据向后挪一位for (int i = ps->size; i > pos; i--){ps->arr[i] = ps->arr[i - 1];}//接着插入你想插入的数字ps->arr[pos + 1] = x;ps->size++;
}//删除pos位置的数据
void SLErase(SL* ps, int pos)
{assert(ps);//pos:[0,ps->size)assert(pos >= 0 && pos < ps->size);//pos后面的数据向前挪一位for (int i = pos; i < ps->size - 1; i++){ps->arr[i] = ps->arr[i + 1];}ps->size--;//相当于我们没有真的去删除一个数,而是被其他数覆盖了
}//改变pos位置的数据
void SLModify(SL* ps, int pos, SLTDataType x)
{assert(ps);//pos:[0,ps->size)assert(pos >= 0 && pos < ps->size);//将pos位置的数据改为xps->arr[pos] = x;
}
3.3、test.c
#include"SeqList.h"
void test01()
{SL s1;SLInit(&s1);//具备了一个空的顺序表//尾插SLPushBack(&s1, 1);//1SLPushBack(&s1, 2);//1 2SLPushBack(&s1, 3);//1 2 3SLPushBack(&s1, 4);//1 2 3 4//头插 SLPushFront(&s1, 1);//1SLPushFront(&s1, 2);//2 1SLPushFront(&s1, 3);//3 2 1SLPushFront(&s1, 4);//4 3 2 1SLPushFront(NULL, 5);//尾删(假设现在顺序表中有1 2 3 4)SLPopBack(&s1);//1 2 3SLPrint(&s1);SLPopBack(&s1);//1 2SLPrint(&s1);SLPopBack(&s1);//1SLPrint(&s1);SLPopBack(&s1);//SLPrint(&s1);//尾删完所有元素,继续尾删就会assertSLPopBack(&s1);//头删(假设顺序表中有1 2 3 4)SLPopFront(&s1);//2 3 4SLPrint(&s1);SLPopFront(&s1);//3 4SLPrint(&s1);SLPopFront(&s1);//4SLPrint(&s1);SLPopFront(&s1);//SLPrint(&s1);//头删完所有元素,继续尾删就会assertSLPopFront(&s1);//查找--要传入s1的地址以及我们要查找的值int pos = SLFind(&s1, 2);if (pos < 0){printf("未找到\n");}else{printf("找到了\n");}//指定位置之前插入,原来顺序表是1 2 3 4//我们想在2之前插入SLInsert(&s1, pos, 100);SLPrint(&s1);//1 100 2 3 4SLInsert(&s1, pos, 200);SLPrint(&s1);//1 200 100 2 3 4SLInsert(&s1, pos, 300);SLPrint(&s1);//1 300 200 100 2 3 4 SLInsert(&s1, pos, 400);SLPrint(&s1);//1 400 300 200 100 2 3 4//指定位置之后插入,原来顺序表是1 2 3 4//我们想在2之后插入SLInsertAfter(&s1, pos, 100);SLPrint(&s1);//1 2 100 3 4SLInsertAfter(&s1, pos, 200);SLPrint(&s1);//1 2 200 100 3 4SLInsertAfter(&s1, pos, 300);SLPrint(&s1);//1 2 300 200 100 3 4 SLInsertAfter(&s1, pos, 400);SLPrint(&s1);//1 2 400 300 200 100 3 4//删除pos位置的数据//删除了2,实际上2被3覆盖了SLErase(&s1, pos);SLPrint(&s1);//改变pos位置的数据//将2改为100SLModify(&s1, pos, 100);SLPrint(&s1);//销毁SLDestroy(&s1);
}int main()
{test01();return 0;
}
这里代码比较多大家可以像我一样复制到VS中然后慢慢理解:
四、顺序表算法题(难点)
4.1、移除元素
https://leetcode.cn/problems/remove-element/description/
思路一:常规思路
申请新数组,遍历原数组,将不为val的值依次放入新数组中,再将新数组中的数据导入到原数组中。
空间复杂度:O(N)
时间复杂度:O(N)
int removeElement(int* nums, int numsSize, int val) {// 用于存储不等于 val 的元素int tmp[numsSize];int k = 0;// 遍历原数组,将不等于 val 的元素放入 tmp 数组for (int i = 0; i < numsSize; i++){if (nums[i] != val){tmp[k] = nums[i];k++;}}// 将 tmp 数组中的元素导回原数组for (int i = 0; i < k; i++){nums[i] = tmp[i];}return k;
}
当然还可以进行进一步的优化:
int removeElement(int* nums, int numsSize, int val) {// 动态分配临时数组内存(长度为 numsSize)int* tmp = (int*)malloc(numsSize * sizeof(int));if (tmp == NULL) { // 内存分配失败的容错return 0;}int k = 0;// 遍历原数组,将不等于 val 的元素放入 tmp 数组for (int i = 0; i < numsSize; i++) {if (nums[i] != val) {tmp[k++] = nums[i];}}// 将 tmp 数组中的元素导回原数组for (int i = 0; i < k; i++) {nums[i] = tmp[i];}free(tmp); // 释放临时数组的内存,避免内存泄漏return k;
}
思路二:双指针法
我们创建俩个变量dst与src,src在前面探路找非val的值。dst在后面保存非val的值,如果src指向的数据是val,src++;如果src指向的数据不是val,赋值(src给dst),src和dst都++。
空间复杂度:O(1)
时间复杂度:O(N)
int removeElement(int* nums, int numsSize, int val) {//创建俩个变量int src = 0, dst = 0;while(src < numsSize){//src的值为val,src++//src的值不为val,赋值再整体++if(nums[src] != val){nums[dst] = nums[src];dst++;}src++;}return dst;
}
4.2、删除有序数组中的重复项
https://leetcode.cn/problems/remove-duplicates-from-sorted-array/description/
思路一:常规思路
创建新数组,遍历原数组,将不重复数据导入到新数组中,再将新数组中的数据导入到原数组中。
空间复杂度:O(N)
时间复杂度:O(N)
int removeDuplicates(int* nums, int numsSize) {if (numsSize == 0) {return 0;}// 动态分配新数组内存int* newNums = (int*)malloc(numsSize * sizeof(int));if (newNums == NULL) {return 0;}int j = 0;newNums[j++] = nums[0];// 遍历原数组,将不重复元素放入新数组for (int i = 1; i < numsSize; i++) {if (nums[i] != nums[i - 1]){newNums[j++] = nums[i];}}// 将新数组元素导回原数组for (int i = 0; i < j; i++){nums[i] = newNums[i];}free(newNums);return j;
}
思路二:双指针法
创建俩个变量,分别指向数组的起始位置(dst)和下一个位置(src),如果src的值和dst的值相等,src++;如果src的值和dst的值不相等,dst++,赋值(dst=src),src++。
int removeDuplicates(int* nums, int numsSize) {//创建俩个变量int dst = 0, src = dst + 1;while(src < numsSize){//src和dst的值相等,src++//src的值和dst的值不相等,dst++,赋值(dst=src),src++if(nums[src]!=nums[dst]){dst++;nums[dst] = nums[src];}src++;}return dst+1;
}
这段代码已经没有啥问题了,但是我们可以进一步优化:
int removeDuplicates(int* nums, int numsSize) {//创建俩个变量int dst = 0, src = dst + 1;while(src < numsSize){//src和dst的值相等,src++//src的值和dst的值不相等,dst++,赋值(dst=src),src++if(nums[src]!=nums[dst]){dst++;if(src!=dst){nums[dst] = nums[src];}}src++;}return dst+1;
}
空间复杂度:O(1)
时间复杂度:O(N)
4.3、合并两个有序数组
https://leetcode.cn/problems/merge-sorted-array/description/
思路一:常规思路
我们可以先合并数组,再对数组一进行排序(qsort、冒泡排序)
qsort
// 比较函数,用于 qsort
int compare(const void* a, const void* b) {return (*(int*)a - *(int*)b);
}void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {// 将 nums2 中的元素合并到 nums1 中for (int i = 0; i < n; i++) {nums1[m + i] = nums2[i];}// 对合并后的 nums1 进行排序qsort(nums1, m + n, sizeof(int), compare);
}
空间复杂度:O(1)
时间复杂度:O(N)
冒泡排序
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {// 1. 合并 nums2 到 nums1for (int i = 0; i < n; i++) {nums1[m + i] = nums2[i];}// 2. 冒泡排序合并后的 nums1for (int i = 0; i < m + n - 1; i++) {for (int j = 0; j < m + n - 1 - i; j++) {if (nums1[j] > nums1[j + 1]) {int temp = nums1[j];nums1[j] = nums1[j + 1];nums1[j + 1] = temp;}}}
}
空间复杂度:O(1)
时间复杂度:O(N^2)
思路二:顺序表尾插
从后向前遍历数组,找大(谁大谁放后)
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] > nums2[l2]){nums1[l3] = nums1[l1];l3--;l1--;}else{nums1[l3] = nums2[l2];l3--;l2--;}}//l1越界(l2没有越界,需要特殊处理)while(l2 >= 0){nums1[l3--] = nums2[l2--];}//l2越界(不需要处理)//l1、l2同时越界(不存在)
}
空间复杂度:O(1)
时间复杂度:O(N)
五、顺序表问题与思考
- 中间 / 头部的插入删除,时间复杂度为 O (N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈 2 倍的增长,势必会有一定的空间浪费。例如当前容量为 100,满了以后增容到 200,我们再继续插入了 5 个数据,后面没有数据插入了,那么就浪费了 95 个数据空间。
思考:如何解决以上问题呢?
有没有一种数据结构满足:1、头部插入删除,时间复杂度0(1);2、不需要增容;3、不存在空间浪费。
答案是有的,这就是链表,下一篇博客将为大家继续讲解。