数据结构
定义
1. 基本概念
数据结构是计算机存储、组织数据的方式, 是指相互之间存在一种或多种特定关系的数据元素的集合。它研究的是数据的 逻辑结构 和 物理结构 以及他们之间的相互关系, 并对这种结构定义相应的运算, 设计出相应的算法, 所以数据结构和算法也是不可分割的。
2. 相关术语
数据结构中常见的几个概念, 先放一张偷来的图:

- 数据
描述客观事物的符号, 能够被计算机识别, 是计算机中可以操作的对象。包括 整型、浮点型等数值类型, 也包括 字符、声音、视频等非数值类型。
- 数据项
数据不可分割的最小标识单位, 用来组成数据元素。数据线通常不具有完整确定的实际意义, 或不被当做一个整体对待, 数据项又被成为 字段或域。
- 数据元素
数据元素是数据的基本单位, 在程序中作为一个整体考虑和处理, 是运算的基本单位。通常具有完整的实际意义, 又被成为 元素、结点、顶点或记录。
- 数据对象
具有相同性质的数据元素的集合, 是数据的子集。
性质相同: 指数据元素具有相同数量和类型的数据项, 类似数组中的元素保持性质一致。
下面通过两个表格来通俗的理解一下上面的概念:
| 姓名 | 性别 | 身高(cm) |
|---|---|---|
| 李磊 | 男 | 180 |
| 韩梅梅 | 女 | 260 |
| 课程编号 | 课程名 |
|---|---|
| 1001 | 语文 |
| 1002 | 数学 |
-
上面我们定义了两张表, 表A为人员表, 表B为课程表, 这两张表就是 数据
-
单独的一张表的话, 就是我们说的 数据对象
-
每张表的每一行就称为 数据元素
-
组成 数据元素 的部分, 比如姓名, 性别, 身高等, 这些称为 数据项
逻辑结构
逻辑结构指的是数据元素之间的相互关系 。根据相互关系的不同, 逻辑结构分为四种类型: 集合结构, 线性结构, 树形结构, 图形结构。
1. 集合结构

数据元素同属于一个集合, 之间没有任何其他联系, 各个元素之间都是平等的。
2. 线性结构

线性结构中的数据元素之间是 一对一的关系, 常见的线性结构有: 线性表, 栈, 队列, 数组等。
3. 树形结构

树形结构可以表示数据元素之间 一对多的关系 , 是很重要的 非线性数据结构。常见的树形结构有: 二叉树, B树, 红黑树等。
4. 图形结构

图形结构数据元素是 多对多的关系 。常见的图形结构有: 邻近矩阵等。
物理结构
物理结构 (又称"存储结构"), 指的是数据的逻辑结构在计算机中的存储形式。存储形式主要有2种: 顺序存储 和 链式存储。
1. 顺序存储结构

顺序存储结构是把数据元素放到地址连续的存储单元里面, 数据之间的逻辑关系和物理关系是一致的。个人理解顺序就是指相邻数据元素之间的地址连续, 类似于排队一样。
顺序存储结构就像我们生活中排队一样, 假如中间有人插队或者有人离队的话, 整个结构都需要发生变化, 这时候就需要引入链式存储结构了。
2. 链式存储结构

链式存储结构把数据元素放在任意的存储单元里, 这些存储单元可以是连续的, 也可以是不连续的。数据元素的存储关系并不能反映逻辑关系, 因此这里引入了指针, 用一个指针存放数据元素的地址, 这样通过地址就可以找到相关联数据元素的位置。
很显然链式结构比较灵活, 数据存储位置不重要, 只要指针存放了相应的地址, 就可以通过指针找到相邻元素了。从这一点也能看到 顺序存储结构 和 链式存储结构 最大的区别就是链式存储结构引进了指针的概念,通过指针来存储地址,从而通过地址来寻找相邻元素。
常用的数据结构
- 数组 (Array)
数组是一种聚合数据类型,它是将具有相同类型的若干变量有序地组织在一起的集合。数组可以说是最基本的数据结构,在各种编程语言中都有对应。一个数组可以分解为多个数组元素,按照数据元素的类型,数组可以分为整型数组、字符型数组、浮点型数组、指针数组和结构数组等。数组还可以有一维、二维以及多维等表现形式。
- 栈 (Stack)
栈是一种特殊的线性表,它只能在一个表的一个固定端进行数据结点的插入和删除操作。栈按照后进先出的原则来存储数据,也就是说,先插入的数据将被压入栈底,最后插入的数据在栈顶,读出数据时,从栈顶开始逐个读出。栈在汇编语言程序中,经常用于重要数据的现场保护。栈中没有数据时,称为空栈。
- 队列 (Queue)
队列和栈类似,也是一种特殊的线性表。和栈不同的是,队列只允许在表的一端进行插入操作,而在另一端进行删除操作。一般来说,进行插入操作的一端称为队尾,进行删除操作的一端称为队头。队列中没有元素时,称为空队列。
- 链表 (Linked List)
链表是一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在物理上存在非连续的特点。链表由一系列数据结点构成,每个数据结点包括数据域和指针域两部分。其中,指针域保存了数据结构中下一个元素存放的地址。链表结构中数据元素的逻辑顺序是通过链表中的指针链接次序来实现的。
- 树 (Tree)
树是典型的非线性结构,它是包括,2个结点的有穷集合K。在树结构中,有且仅有一个根结点,该结点没有前驱结点。在树结构中的其他结点都有且仅有一个前驱结点,而且可以有两个后继结点,m≥0。
- 图 (Graph)
图是另一种非线性数据结构。在图结构中,数据结点一般称为顶点,而边是顶点的有序偶对。如果两个顶点之间存在一条边,那么就表示这两个顶点具有相邻关系。
- 堆 (Heap)
堆是一种特殊的树形数据结构,一般讨论的堆都是二叉堆。堆的特点是根结点的值是所有结点中最小的或者最大的,并且根结点的两个子树也是一个堆结构。
- 散列表 (Hash)
散列表源自于散列函数(Hash function),其思想是如果在结构中存在关键字和T相等的记录,那么必定在F(T)的存储位置可以找到该记录,这样就可以不用进行比较操作而直接取得所查记录。
算法
算法就是描述解决问题的方法, 人们在实际开发中会遇到很多的问题, 解决问题的方法也是千变万化。每个人对问题的解决方案都是不一样的, 不同的算法可能用不同的时间、空间或效率来完成同样的任务, 一个算法的优劣可以用 空间复杂度 与 时间复杂度 来衡量。
特性
一个算法必须具备下面的几个基本特性:
- 输入输出
输入用来获取对象的已知条件, 输出用来反映算法执行后的结果。一个算法可以有 0 个输入(算法本身定出了初始条件), 但是不可以没有输出, 没有输出的算法是毫无意义的。
- 有穷性
指算法在执行有限的步骤之后自动结束, 而不是无限执行下去。
- 确定性
算法的每一步都具有确定的含义, 需要保证算法的相同条件下只有一条执行路径, 相同的输入只能有唯一的输出结果
- 可行性
也称为有效性, 一个算法是可行的, 即算法中描述的操作都是通过已经实现的基本运算执行有限次来实现的。
设计要求
在不考虑效率的情况下, 一个合格的算法必须满足下面的三个条件:
- 正确性
算法的正确性是评价一个算法优劣的最重要的标准。
- 可读性
可读性是指一个算法可供人们阅读的容易程度, 可读性高有助于人们理解算法; 晦涩难懂的算法往往很难调试和修改。
注意: 代码越少, 越牛逼!!! 这种想法是不可取的, 如今团队协作的今天, 不再是个人英雄主义的时代, 我觉得能让人更容易读懂才是关键。
( 所以, 能加注释的顺手加上吧, 队友很难受的。)
- 健壮性
健壮性是值一个算法对不合理数据输入的反应能力和处理能力, 也成为 容错性 。
- 时间效率和存储效率
这里就是下面要说到的 时间复杂度 与 空间复杂度 , 在保证上面的一切的同时, 还要尽可能的提升算法的 执行效率 和 资源占用情况。
效率度量
时间复杂度
1. 时间复杂度定义
在进行算法分析时, 语句的总执行次数 T(n) 是关于问题规模 n 的函数, 进而分析T(n)随着n变化情况并确定T(n)的数量级, 算法的时间复杂度, 也就是算法的时间量度
T(n) = O(f(n))他表示随着问题的规模 n 的增大, 算法执行时间的增长率和 f(n) 的增长率相同, 成为算法的渐进时间复杂度, 简称时间复杂度。(这种使用 O( ) 来体现算法时间复杂度的记法, 我们称之为大O表示法。)
2. 大 O 表示法
- 大 O 表示法规则
/*大O表示法
1. 用常数1取代运行时间中所有常数 3->1 O(1)
2. 在修改运行次数函数中,只保留最高阶项 n^3+2n^2+5 -> O(n^3)
3. 如果在最高阶存在且不等于1,则去除这个项目相乘的常数 2n^3 -> n^3
*/
- 使用大 O 表示法计算时间复杂度
时间复杂度总共可以分为: 常数阶, 线性阶, 平方阶, 对数阶, 立方阶, nlog阶, 指数阶
指数阶目前不考虑, 因为如果n增大以后, 时间复杂度会噩梦般的增长, 有点不切实际
- 常数阶
// 1 + 1 + 1 = 3 O(1)
void test1(int n){
int sum = 0; //执行1次
sum = (1+n)*n/2; //执行1次
printf("testSum1:%d\n",sum);//执行1次
}
// 不管传进来的 n 为多少, 程序执行的代码都是固定的, 所以时间复杂度为常数阶, O(1)
- 线性阶
// 执行 n 次 x = x+1; O(n)
void test2(int x,int n){
for (int i = 0; i < n; i++) {
x = x+1;
}
}
// 根据传进来的 n 的值成 n 倍数增长, 时间复杂度为 O(n)
- 对数阶
// 2的x次方等于n x = log2n ->O(logn) ...(log2n 里的 2为下标)
void test3(int n){
int count = 1; //执行1次
while (count < n) {
count = count * 2;
}
}
//
- 平方阶
//x=x+1; 执行n*n次 ->O(n^2)
void test4(int x,int n){
for (int i = 0; i< n; i++) { // n
for (int j = 0; j < n ; j++) { // n * n
x=x+1;
}
}
}
- 立方阶
// 1+n+(n*n)+(n*n*n)+(n*n*n) = 1+n+n^2+2n^3 -> O(n^3)
void test5(int n){
int sum = 1; //执行1次
for (int i = 0; i < n; i++) { //执行n次
for (int j = 0 ; j < n; j++) { //执行n*n次
for (int k = 0; k < n; k++) {//执行n*n*n次
sum = sum * 2; //执行n*n*n次
}
}
}
}
空间复杂度
// 程序存储空间计算因素:
/*
1. 寄存本身的指令
2. 常数
3. 变量
4. 输入
5. 对数据进行操作的辅助空间
*/
算法的空间复杂度通过计算算法所需的存储空间实现, 算法空间复杂度的计算公式为:
S(n) = n(f(n))一般情况下, 程序在机器上执行时除了需要寄存本身的指令, 数据等之外, 还需要一些执行数据操作的辅助存储空间。在考量算法的空间复杂度上,主要考虑算法执行时所需要的辅助空间。
下面通过两个例子来看一下:
// 问题: 将一维数组a中的n个数逆序存放在原数组中.
// 1
int temp;
for(int i = 0; i < n/2 ; i++){
temp = a[i];
a[i] = a[n-i-1];
a[n-i-1] = temp;
}
for(int i = 0;i < 10;i++){
printf("%d\n",a[i]);
}
// 2
int b[10] = {0};
for(int i = 0; i < n;i++){
b[i] = a[n-i-1];
}
for(int i = 0; i < n; i++){
a[i] = b[i];
}
for(int i = 0;i < 10;i++){
printf("%d\n",a[i]);
}
- 算法1声明了一个临时变量
temp, 借助temp解决问题, 与n的大小没有关系, 所以他的空间复杂度为 O(1) 。 - 算法2 借助了一个大小为
n的辅助数组, 所以他的空间复杂度为 O(n) 。
注意: 空间复杂度不是整个算法内存占用空间大小, 而是指在算法实现时需要开辟出的辅助空间占用的大小
总结
本文介绍了关于 数据结构 和 算法 的一些定义, 主要是一些文字性的叙述, 所以有些地方可能表达的不是很准确。如果有不对的地方或者不是很明白的地方, 欢迎进行提问和指出。这里也算是对 数据结构 和 算法 学习的一个开始, 以后会陆续增加更多内容。
和谐学习, 不急不躁 ~