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

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=0nums[0]=10≠30nums[0]=10dst=1
    • src=1nums[1]=20≠30nums[1]=20dst=2
    • src=2nums[2]=30=30 → 跳过,dst 不动
    • src=3nums[3]=40≠30nums[2]=40dst=3
    • src=4nums[4]=50≠30nums[3]=50dst=4
    • src=5nums[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=0dst=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=1nums[0]=10nums[1]=10 重复 → dst++
  • dst=2nums[0]=10nums[2]=30 不重复 → nums[1] = 30src=1, dst=3
  • dst=3nums[1]=30nums[3]=30 重复 → dst++
  • dst=4nums[1]=30nums[4]=30 重复 →dst++
  • dst=5nums[1]=30nums[5]=40 不重复 → nums[2] = 40src=2, dst=6
  • dst=6nums[2]=40nums[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;
}
相关推荐
熬夜敲代码的猫1 小时前
AVL树(C++详解版)
数据结构·c++·算法
并不喜欢吃鱼2 小时前
从零开始 C++-----十一【C++ 数据结构】红黑树全解析:从定义到工程实现(一文搞定,十分详细)
开发语言·数据结构·c++
星恒随风2 小时前
C语言数据结构排序算法详解(上):从插入排序、希尔排序到选择排序、堆排序
c语言·数据结构·笔记·学习·排序算法
迈巴赫车主2 小时前
蓝桥杯21247弹跳鞋java
java·开发语言·数据结构·算法·职场和发展·蓝桥杯
Cthy_hy3 小时前
Python算法竞赛:集合去重+字典映射 核心用法一站式整理
数据结构·python·算法
happymaker06263 小时前
LeetCodeHot100——盛水最多的容器
数据结构·算法·leetcode·双指针·hot100
过期动态3 小时前
【LeetCode 热题 100】三数之和
java·数据结构·算法·leetcode·职场和发展·排序算法
一切皆是因缘际会3 小时前
AI高速迭代下的技术风险与理性突围
大数据·数据结构·人工智能·架构
代码中介商4 小时前
B+树:数据库索引的终极奥秘
数据结构