数据结构开篇:从问题到解决方案

引言

在之前的学习中,我们学习了C语言的基础语法、指针、结构体、动态内存分配,甚至用这些知识实现了一个银行管理系统。但你可能已经发现,随着程序规模的增长,管理数据变得越来越困难。

一个简单的问题: 如果让你存储100个学生的成绩,你会怎么做?

cpp 复制代码
// 方案1:定义100个单独的变量?
int score1, score2, score3, ...;  // ❌ 不可行

// 方案2:使用数组
int scores[100];  // ✅ 可行,但功能有限

数组能解决一部分问题,但它的局限性也很明显:

  • 大小固定,无法动态扩展

  • 插入、删除元素需要移动大量数据

  • 无法方便地存储复杂的学生信息(姓名、学号、成绩等)

这就是为什么我们需要学习数据结构


第一部分:什么是数据结构?

一、数据结构的定义

数据结构是计算机存储、组织数据的方式。它是指相互之间存在一种或多种特定关系的数据元素的集合。

通俗地说,数据结构就是研究如何高效地存储和管理数据

类比理解:

场景 类比 数据结构
排队买票 先来先服务 队列(Queue)
叠盘子 后进先出 栈(Stack)
通讯录 按顺序存储 数组(Array)
地铁线路图 网状连接 图(Graph)
公司组织架构 树状结构 树(Tree)

二、数据结构的分类

三、逻辑结构与物理结构的区别

结构类型 定义 举例
逻辑结构 数据之间的抽象关系 线性、树形、图形
物理结构 数据在内存中的实际存储方式 顺序存储、链式存储

关键理解: 逻辑结构是"思路",物理结构是"实现"。同一种逻辑结构可以用不同的物理结构实现。


第二部分:为什么需要数据结构?

一、没有数据结构的困境

假设我们要存储一个班级的学生信息,需要支持:

  • 添加新学生

  • 删除指定学生

  • 按学号查找

  • 按成绩排序

如果只用数组,实现起来非常复杂:

cpp 复制代码
// 使用数组存储学生信息
typedef struct {
    int id;
    char name[20];
    int score;
} Student;

Student students[100];
int count = 0;

// 插入学生:需要移动后面的所有元素
void insert(int pos, Student s) {
    for (int i = count; i > pos; i--) {
        students[i] = students[i - 1];
    }
    students[pos] = s;
    count++;
}

// 删除学生:也需要移动元素
void delete(int pos) {
    for (int i = pos; i < count - 1; i++) {
        students[i] = students[i + 1];
    }
    count--;
}

这种实现方式存在明显问题:

  • 插入/删除操作效率低(O(n))

  • 数组大小固定,扩容不便

  • 代码容易出错(边界问题)

二、数据结构解决的问题

问题 解决方案 相关数据结构
数据存储 如何组织数据以便访问 数组、链表
数据插入/删除 如何在中间位置操作 链表
数据查找 如何快速找到目标数据 树、哈希表
数据排序 如何按规则排列数据 堆、二叉搜索树
数据顺序 如何按特定规则处理 栈、队列

第三部分:数据结构和算法的关系

一、程序 = 数据结构 + 算法

这是计算机科学领域的经典公式。数据结构决定了数据的组织方式,算法决定了对数据的操作方式。

二、同一个问题,不同的解法

问题: 判断一个字符串是否是回文(正读反读相同)

解法1:使用数组

cpp 复制代码
int isPalindrome(char str[], int len) {
    for (int i = 0; i < len / 2; i++) {
        if (str[i] != str[len - 1 - i]) return 0;
    }
    return 1;
}

解法2:使用栈

cpp 复制代码
int isPalindrome(char str[], int len) {
    Stack s;
    initStack(&s);
    
    // 前半部分入栈
    for (int i = 0; i < len / 2; i++) {
        push(&s, str[i]);
    }
    
    // 后半部分与栈顶比较
    int start = (len + 1) / 2;
    for (int i = start; i < len; i++) {
        if (pop(&s) != str[i]) return 0;
    }
    return 1;
}

不同的数据结构适用于不同的场景。数组简单直接,而栈更好地体现了"后进先出"的逻辑。


第四部分:时间复杂度和空间复杂度

一、为什么要学习复杂度?

当我们有多种方案解决同一个问题时,如何选择最优的方案?复杂度分析就是答案。

复杂度 含义 通俗解释
时间复杂度 算法执行所需时间 运行快慢
空间复杂度 算法执行所需内存 占用多少内存

二、大O表示法

大O表示法用于描述算法在最坏情况下的时间复杂度。

复杂度 名称 例子 数据量翻倍时
O(1) 常数阶 数组访问 时间不变
O(log n) 对数阶 二分查找 时间增加约1倍
O(n) 线性阶 遍历数组 时间翻倍
O(n log n) 线性对数阶 快速排序 时间增加略多于2倍
O(n²) 平方阶 冒泡排序 时间变为4倍

时间复杂度对比图(直观理解):

三、复杂度计算示例

cpp 复制代码
// O(1) - 常数阶
int getFirst(int arr[]) {
    return arr[0];  // 只执行1次操作
}

// O(n) - 线性阶
int sum(int arr[], int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {  // 循环n次
        total += arr[i];
    }
    return total;
}

// O(n²) - 平方阶
void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; i++) {      // 外层n次
        for (int j = 0; j < n - 1 - i; j++) {  // 内层n次
            if (arr[j] > arr[j + 1]) {
                swap(&arr[j], &arr[j + 1]);
            }
        }
    }
}

第五部分:数据结构学习路线图

一、建议学习顺序

二、每个阶段的目标

阶段 核心目标 实践项目
线性结构 理解顺序存储和链式存储的区别 实现一个简单的图书管理系统
栈和队列 理解后进先出和先进先出的应用场景 实现括号匹配、任务调度
查找与排序 掌握不同算法的时间和空间复杂度 实现学生成绩排序
树形结构 理解层次结构的数据组织 实现文件目录遍历
图结构 理解网络关系的数据建模 实现地铁线路规划

第六部分:我们已经学过的数据结构

在学习数据结构之前,其实我们已经接触过一些数据结构了:

数据结构 我们已经学过的内容 待深入的内容
数组 一维/多维数组的定,内存布局 动态数组、变长数组
结构体 自定义数据类型,成员访问 结构体数组的管理
函数调用栈(运行时) 自定义栈的实现(顺序栈、链式栈)
队列 未正式学习 顺序队列、循环队列
链表 未正式学习 单向链表、双向链表

栈的应用举例:函数调用栈

cpp 复制代码
// 下面的函数调用过程,就是一个栈的应用
void funcC() { }      // 第3个入栈,第1个出栈
void funcB() { funcC(); }  // 第2个入栈,第2个出栈
void funcA() { funcB(); }  // 第1个入栈,第3个出栈

int main() {
    funcA();  // 函数调用栈:main → funcA → funcB → funcC → 返回
    return 0;
}

这就是为什么数据结构如此重要------它不仅是我们显式使用的工具,更是程序运行的基础机制。


第七部分:学习数据结构的建议

一、学习方法

方法 说明
理解原理 先弄清楚数据结构的设计思想,再动手实现
动手编码 不要只看代码,每个数据结构都要自己实现一遍
画图辅助 数据结构涉及很多指针操作,画图能帮你理清思路
分析复杂度 实现之后,分析时间和空间复杂度
实际应用 将学到的数据结构应用到实际项目或练习题中

二、常见误区

误区 正确理解
数据结构是死记硬背 数据结构是解决问题的工具,重在理解
理论比实践重要 理论和实践同等重要,缺一不可
越复杂的结构越好 适用于场景的才是最好的
直接抄代码就行 抄一遍远不如自己写一遍理解深刻

第八部分:动手预热------从一个简单的问题开始

问题: 实现一个动态整数数组,支持以下操作:

学习建议:

数据结构的学习需要时间和耐心,但一旦掌握,你会发现编程世界变得更加清晰。下一篇文章,我们将从最简单的线性表开始,实现一个完整的链表。

  • init:初始化数组

  • add:在末尾添加元素

  • insert:在指定位置插入元素

  • removeAt:删除指定位置的元素

  • get:获取指定位置的元素

  • destroy:销毁数组

    cpp 复制代码
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define INIT_CAPACITY 4
    
    typedef struct {
        int* data;
        int size;      // 当前元素个数
        int capacity;  // 当前容量
    } DynamicArray;
    
    // 初始化
    void init(DynamicArray* arr) {
        arr->data = (int*)malloc(INIT_CAPACITY * sizeof(int));
        arr->size = 0;
        arr->capacity = INIT_CAPACITY;
    }
    
    // 扩容
    void expand(DynamicArray* arr) {
        int newCapacity = arr->capacity * 2;
        int* newData = (int*)realloc(arr->data, newCapacity * sizeof(int));
        if (newData == NULL) return;
        arr->data = newData;
        arr->capacity = newCapacity;
        printf("扩容成功: %d -> %d\n", arr->capacity / 2, arr->capacity);
    }
    
    // 在末尾添加
    void add(DynamicArray* arr, int val) {
        if (arr->size >= arr->capacity) {
            expand(arr);
        }
        arr->data[arr->size++] = val;
    }
    
    // 在指定位置插入
    void insert(DynamicArray* arr, int pos, int val) {
        if (pos < 0 || pos > arr->size) return;
        if (arr->size >= arr->capacity) {
            expand(arr);
        }
        // 移动元素
        for (int i = arr->size; i > pos; i--) {
            arr->data[i] = arr->data[i - 1];
        }
        arr->data[pos] = val;
        arr->size++;
    }
    
    // 删除指定位置元素
    void removeAt(DynamicArray* arr, int pos) {
        if (pos < 0 || pos >= arr->size) return;
        for (int i = pos; i < arr->size - 1; i++) {
            arr->data[i] = arr->data[i + 1];
        }
        arr->size--;
    }
    
    // 获取元素
    int get(DynamicArray* arr, int pos) {
        if (pos < 0 || pos >= arr->size) return -1;
        return arr->data[pos];
    }
    
    // 打印数组
    void print(DynamicArray* arr) {
        for (int i = 0; i < arr->size; i++) {
            printf("%d ", arr->data[i]);
        }
        printf("\n");
    }
    
    // 销毁
    void destroy(DynamicArray* arr) {
        free(arr->data);
        arr->data = NULL;
        arr->size = 0;
        arr->capacity = 0;
    }
    
    int main() {
        DynamicArray arr;
        init(&arr);
        
        for (int i = 0; i < 10; i++) {
            add(&arr, i + 1);
        }
        printf("添加后: ");
        print(&arr);
        
        insert(&arr, 3, 100);
        printf("插入后: ");
        print(&arr);
        
        removeAt(&arr, 5);
        printf("删除后: ");
        print(&arr);
        
        destroy(&arr);
        return 0;
    }

    这是一个动态数组 的简单实现,它结合了数组的连续存储特性和动态扩容能力。这个例子很好的展示了数据结构设计的核心思想------封装数据和对数据的操作

    数据结构是计算机科学的核心课程,它不仅仅是面试中的必考内容,更是编程内功的重要组成部分。

    本文作为数据结构系列的开篇,旨在为你解答"为什么需要学数据结构"这个问题。后续我们将会逐一深入讲解:

  • 线性表(链表、数组)

  • 栈和队列

  • 树和图

  • 查找与排序算法

  • 每个数据结构都要自己动手实现一遍

  • 画图理解指针操作,不要只在脑中想象

  • 分析每个操作的时间复杂度和空间复杂度

  • 思考每个数据结构适用于什么场景

    • 哈希表
相关推荐
AKDreamer_HeXY2 小时前
QOJ 12255 - 36 Puzzle 题解
数据结构·c++·数学·算法·icpc·qoj
Rabitebla2 小时前
vector 的骨架:三根指针、模板陷阱与迭代器失效的第一现场
开发语言·数据结构·c++·算法
Sarvartha3 小时前
N 个字符串最长公共子序列(LCS)求解问题
数据结构·算法
m0_629494733 小时前
LeetCode 热题 100-----16.除了自身以外数组的乘积
数据结构·算法·leetcode
迷途之人不知返4 小时前
优先级队列:priority_queue
数据结构·c++
jieyucx4 小时前
Go 零基础数据结构:顺序表(像「排抽屉」一样学增删改查)
java·数据结构·golang
想唱rap4 小时前
应用层协议与序列化
linux·运维·服务器·网络·数据结构·c++·算法
jinyishu_5 小时前
链表经典OJ题
c语言·数据结构·算法·链表
澈2075 小时前
C++引用与指针:核心区别全解析
开发语言·数据结构·c++