数据结构从入门到精通:顺序表

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(写入指针,记录下一个要写入的位置)。**
  • 遍历数组:
    1. 如果 nums[src] != val,说明这个元素要保留,把它赋值给 nums[dst],然后 dst++。
    2. 如果 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=1
    • src=1,nums[1]=20≠30 → nums[1]=20,dst=2
    • src=2,nums[2]=30=30 → 跳过,dst 不动
    • src=3,nums[3]=40≠30 → nums[2]=40,dst=3
    • src=4,nums[4]=50≠30 → nums[3]=50,dst=4
    • src=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:遍历整个数组(快指针)

核心逻辑

  1. 初始时 src=0,dst=1
  2. 遍历数组:
    • 如果**nums[src] == nums[dst]** :说明是重复元素 ,直接 dst++ 跳过
    • 如果 nums[src] != nums[dst]:说明找到新的不重复元素 ,执行 nums[++src] = nums[dst++] (把新元素写到 src 的下一个位置 ,两个指针同时后移)
  3. 遍历结束后,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=3
  • dst=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=6
  • dst=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;
}
相关推荐
Bruce_Liuxiaowei10 分钟前
元组与具名元组:把「第 3 个字段」变成「price」
数据结构·python·语法·元组
今晚打老虎11 小时前
c++之提高A(前缀和)(第三课)
数据结构·c++·算法
不会就选b12 小时前
算法日常・每日刷题--<贪心>26
数据结构·算法
Logic10116 小时前
C语言/数据结构位运算题解:异或XOR找出书柜中编号只出现一次的那一层——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
青山木16 小时前
Hot 100 --- 下一个排列
java·数据结构·算法·leetcode
SelectDB技术团队18 小时前
Apache Doris 高性能 Open Lake Variant 读写技术解析(含对比数据)
数据结构·数据库·clickhouse·日志·apache doris·selectdb·湖仓架构升级
kiracrimson18 小时前
希尔排序:让插入排序不再怕逆序
数据结构·算法·排序算法
Logic10118 小时前
C语言/数据结构位运算题解:异或XOR找出星球探险中的“独特能量石“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
hnjzsyjyj21 小时前
洛谷 P1339:[USACO09OCT] Heat Wave G ← Bellman-Ford 算法
数据结构·bellman-ford 算法
SelectDB技术团队1 天前
ClickHouse 存日志的能力边界:并发、检索与 trace 回放的实测对照
数据结构·数据库·clickhouse·日志分析·apache doris·日志存储