1. 线性表
- 定义 :n 个具有相同特性的数据元素的有限序列,是最基础的线性数据结构。
- 逻辑结构 :元素之间是一对一的线性关系,逻辑上呈连续的 "直线" 结构。(人为想象出来的结构比如吃饭排队)
- 物理存储 :不一定连续,常见的实现方式分为顺序存储(数组)和链式存储(链表) 。(比如数组地址内存是连续存储的)
- 常见类型 :顺序表、链表、栈、队列、字符串等都属于线性表的范畴。
2. 顺序表(SeqList--sequence list)
一、顺序表的定义以及与数组的区别
- 定义 :顺序表是用一段物理地址连续 的存储单元,依次存储数据元素的线性结构,底层通常用数组实现。
- 本质类比 :
- 数组就像 "苍蝇馆子" 的炒西蓝花,只有最基础的食材和做法;
- 顺序表就像米其林餐厅的 "绿野仙踪" ,是对数组的封装 ,加上了增、删、改、查等完整接口,还会管理有效数据长度和容量。
- 和数组的区别 :数组 是编程语言提供的基础存储结构 ,而顺序表是数据结构层面的抽象 ,它在数组的基础上,额外实现了增删改查等接口,更安全、易用。
二、顺序表的两种分类
1. 静态顺序表(定长数组实现)
typedef int SLDataType;
#define N 7 // 固定最大容量
typedef struct SeqList
{
SLDataType a[N]; // 定长数组,物理地址连续
int size; // 有效数据个数
} SL;
这里只给数组的类型 为 typedef定义的别名(方便以后一键修改),而size表示的就是有效数据的个数一直是int类型
- 特点 :容量在编译时就固定死了,用的是栈上的数组。
- 缺陷 :
- 空间给少了:数据量超过
N就会溢出; - 空间给多了:数据量远小于
N,造成内存浪费。
- 空间给少了:数据量超过
- 适用场景 :数据量提前已知且固定的场景。
2. 动态顺序表(动态数组实现)
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a; // 指向动态分配的数组
int size; // 有效数据个数
int capacity; // 当前数组的总容量
} SL;
- 特点 :用**
malloc在堆上分配内存**,支持按需扩容。 - 优势 :
- 不会提前浪费空间,初始容量可以很小 (比如
INIT_CAPACITY 4); - 数据量超过容量时,可以自动扩容(通常扩容为原来的 2 倍)。
- 不会提前浪费空间,初始容量可以很小 (比如
- 适用场景 :绝大多数实际开发场景,数据量未知或会动态变化。
三、动态顺序表的接口设计
| 模块 | 接口 | 功能 |
|---|---|---|
| 初始化与销毁 | void SLInit(SL* ps); |
初始化顺序表,分配初始内存 |
void SLDestroy(SL* ps); |
释放动态数组的内存,防止内存泄漏 | |
void SLPrint(SL* ps); |
打印顺序表所有元素,调试用 | |
| 扩容 | void SCheckCapacity(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 SLErase(SL* ps, int pos); |
删除指定位置的元素 | |
int SLFind(SL* ps, SLDataType x); |
查找元素,返回下标 |
四、代码小提示(重要!)
"编写代码过程中要勤测试,避免写出大量代码后再测试而导致出现问题,问题定位无从下手。"这是数据结构开发的核心原则:写一个功能,测一个功能 。比如先实现SLInit,再实现SLPushBack,每一步都用printf或调试器验证,避免最后堆在一起全是 bug。
五、动态顺序表核心代码实现(可直接运行)
用动态顺序表实现尾插 头插 尾删 头删 指定位置前插入数据 以及删除指定位置的数据
SeqList.h(函数声明)
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
//定义动态顺序表的结构
typedef struct SeqList
{
SLDataType* arr;
int size;
int capacity;
}SL;
// typedef struct SeqList SL;
//打印顺序表
void SLPrint(SL*ps);
//顺序表的初始化
void SLInit(SL *ps);
//尾插
void SLPushBack(SL* ps,SLDataType x);
//头插
void SLPushFront(SL*ps,SLDataType x);
//尾删
void SLPopBack(SL*ps);
//头删
void SLPopFront(SL* ps);
//指定位置之前插⼊数据
void SLInsert(SL* ps, int pos, SLDataType x);
//删除i位置的元素并返回
SLDataType SqListDelete(SL*ps ,int i);
SeqList.c(函数实现)
cpp
#include"SeqList.h"
//初始化
void SLInit(SL *ps)
{
ps->arr =NULL;
ps->size=ps->capacity=0;
}
//判断空间是否足够
void SLCheckCapacity(SL*ps)
{
if(ps->size == ps->capacity)
{
int newCapacity =ps->capacity==0 ? 4 : 2*ps->capacity;
//增容 增容一般成倍数增加 推荐二倍增容
SLDataType* tmp=(SLDataType*)realloc(ps->arr,newCapacity*sizeof(SLDataType));
if(tmp==NULL)
{
perror("realloc fail!");
exit(1);//停止程序 相当于return 1;
}
ps->arr=tmp;
ps->capacity=newCapacity;
}
}
//尾插 时间复杂度O(1)
void SLPushBack(SL* ps,SLDataType x)
{
assert(ps);
// if(ps==NULL)
// {
// return 1;
// }
SLCheckCapacity(ps);
//空间足够的情况下
ps->arr[ps->size]=x;
++ps->size;//->的优先级 高于++
}
//头插 时间复杂度O(N)
void SLPushFront(SL*ps,SLDataType x)
{
assert(ps);
// if(ps==NULL)
// {
// return 1;
// }
SLCheckCapacity(ps);
for(int i =ps->size;i>0;i--)
{
ps->arr[i]=ps->arr[i-1];
}
//空间足够的情况
ps->arr[0]=x;
++ps->size;
}
//尾删 时间复杂度O(1)
void SLPopBack(SL* ps)
{
assert(ps && ps->size);
--ps->size;//有效数字减少相当于删除尾巴
}
//头删 时间复杂度O(N)
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;
}
//头删 时间复杂度O(N)
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos>=0 && pos<=ps->size);
//注意空间
SLCheckCapacity(ps);
//pos及后面的数据整体向后挪动一位
for(int i = ps->size ; i>pos ; i--)
{
ps->arr[i]=ps->arr[i-1];
}
ps->arr[pos]=x;
++ps->size;
}
//删除顺序表中的第i个元素 并返回删除的值
SLDataType SqListDelete(SL*ps ,int i)
{
assert(ps);
assert(i<ps->size && i>=0);
SLDataType del=ps->arr[i];
//挪动数据覆盖
for(int j=i+1;j<ps->size;++j)
{
ps->arr[j-1]=ps->arr[j];
}
--ps->size;
return del;//返回被删除的值
}
//打印顺序表
void SLPrint(SL* ps)
{
for(int i=0;i<ps->size;i++)
{
printf("%d ",ps->arr[i]);
}
printf("\n");
}
3. 移除元素
一、三种思路对比
| 思路 | 核心做法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|---|
| 思路 1 | 遍历数组,遇到等于 val 的元素,就把后面的元素往前移动覆盖 |
O(N²) | O(1) | 原地修改,但移动操作重复,效率低 |
| 思路 2 | 新建临时数组 tmp,遍历原数组,把不等于 val 的元素复制进去,再拷贝回原数组 |
O(N) | O(N) | 线性时间,但需要额外空间 |
| 思路 3(最优) | 双指针原地删除 :src 遍历原数组 ,遇到不等于 val 的元素,就覆盖到 dst 位置 ,dst 同步后移 |
O(N) | O(1) | 时间和空间都最优,原地修改 |
二、最优思路(双指针法)详解
核心逻辑
- 定义两个指针 :
src(遍历指针,从左到右扫描所有元素) 和**dst(写入指针,记录下一个要写入的位置)。** - 遍历数组:
- 如果
nums[src] != val,说明这个元素要保留,把它赋值给nums[dst],然后dst++。 - 如果
nums[src] == val,直接跳过,不执行任何操作。
- 如果
- 遍历结束后,
dst的值就是新数组的长度,原数组前dst个元素就是所有不等于val的元素。
对应图中的例子
原数组 nums = [10, 20, 30, 40, 50, 30],val = 30
src从 0 开始遍历:src=0,nums[0]=10≠30→nums[0]=10,dst=1src=1,nums[1]=20≠30→nums[1]=20,dst=2src=2,nums[2]=30=30→ 跳过,dst不动src=3,nums[3]=40≠30→nums[2]=40,dst=3src=4,nums[4]=50≠30→nums[3]=50,dst=4src=5,nums[5]=30=30→ 跳过,dst不动
- 最终
dst=4,数组前 4 个元素是[10, 20, 40, 50],长度为 4。
三、完整代码实现(C 语言)
#include <stdio.h>
// 双指针法移除元素
int removeElement(int* nums, int numsSize, int val) {
int dst = 0; // 写入指针
for (int src = 0; src < numsSize; src++) {
if (nums[src] != val) {
nums[dst] = nums[src];
dst++;
}
}
return dst;
}
int main() {
int nums[] = {10, 20, 30, 40, 50, 30};
int val = 30;
int numsSize = sizeof(nums) / sizeof(nums[0]);
int newLength = removeElement(nums, numsSize, val);
printf("新数组长度: %d\n", newLength);
printf("新数组元素: ");
for (int i = 0; i < newLength; i++) {
printf("%d ", nums[i]);
}
printf("\n");
return 0;
}
四、代码运行结果
新数组长度: 4
新数组元素: 10 20 40 50
五、补充说明
- 这个方法是**「原地算法」**,不需要额外的数组空间 ,空间复杂度是 O (1)。
- 时间复杂度 是 O (N) ,每个元素只会被
src遍历一次,赋值操作最多执行 N 次。 - 适合数据量大的场景,效率比思路 1 高很多。
4. 删除有序数组中的重复项
一、题目理解
给定一个升序排列 的数组 nums,要求原地删除重复元素 ,让每个元素只出现一次,并返回新数组的长度。
- 空间复杂度要求:O (1)(不能用额外数组)
- 时间复杂度最优:O (N)
二、双指针思路详解
和上一题「移除元素」类似,我们用两个指针实现原地去重:
src:记录去重后有效元素的位置(慢指针)dst:遍历整个数组(快指针)
核心逻辑
- 初始时
src=0,dst=1 - 遍历数组:
- 如果**
nums[src] == nums[dst]** :说明是重复元素 ,直接dst++跳过 - 如果
nums[src] != nums[dst]:说明找到新的不重复元素 ,执行nums[++src] = nums[dst++](把新元素写到src的下一个位置 ,两个指针同时后移)
- 如果**
- 遍历结束后,
src+1就是新数组的长度(因为src是从 0 开始计数的索引)
对应例子
原数组 nums = [10, 10, 30, 30, 30, 40, 40]
- 初始:
src=0, dst=1,nums[0]=10和nums[1]=10重复 →dst++ dst=2,nums[0]=10和nums[2]=30不重复 →nums[1] = 30,src=1, dst=3dst=3,nums[1]=30和nums[3]=30重复 →dst++dst=4,nums[1]=30和nums[4]=30重复 →dst++dst=5,nums[1]=30和nums[5]=40不重复 →nums[2] = 40,src=2, dst=6dst=6,nums[2]=40和nums[6]=40重复 →dst++,遍历结束- 最终
src=2,返回src+1=3,新数组为[10, 30, 40]
三、完整代码实现(C 语言)
#include <stdio.h>
// 双指针法删除有序数组中的重复项
int removeDuplicates(int* nums, int numsSize) {
if (numsSize == 0) { // 空数组特殊处理
return 0;
}
int src = 0, dst = 1;
while (dst < numsSize) {
if (nums[src] != nums[dst]) {
nums[++src] = nums[dst++];
} else {
dst++;
}
}
return src + 1;
}
int main() {
int nums[] = {10, 10, 30, 30, 30, 40, 40};
int numsSize = sizeof(nums) / sizeof(nums[0]);
int newLength = removeDuplicates(nums, numsSize);
printf("新数组长度: %d\n", newLength);
printf("新数组元素: ");
for (int i = 0; i < newLength; i++) {
printf("%d ", nums[i]);
}
printf("\n");
return 0;
}
四、代码运行结果
新数组长度: 3
新数组元素: 10 30 40
五、和「移除元素」的对比
| 题目 | 双指针逻辑 | 核心区别 |
|---|---|---|
| LeetCode 27「移除元素」 | 遍历数组,把不等于 val 的元素写到前面 |
没有 "有序" 的前提,只要不等于就保留 |
| LeetCode 26「删除重复项」 | 遍历数组,把和前一个元素不同的元素写到前面 | 依赖 "有序" 的前提,重复元素一定相邻 |
六、优化写法(简化代码)
可以把 if/else 简化成更紧凑的版本,逻辑不变:
int removeDuplicates(int* nums, int numsSize) {
if (numsSize == 0) return 0;
int i = 0;
for (int j = 1; j < numsSize; j++) {
if (nums[j] != nums[i]) {
nums[++i] = nums[j];
}
}
return i + 1;
}