0. 数据结构概述
什么是数据结构?
简单地说,数据结构是以某种特定的布局方式存储数据的容器。这种“布局方式”决定了数据结构对于某些操作是高效的,而对于其他操作则是低效的。首先我们需要理解各种数据结构,才能在处理实际问题时选取最合适的数据结构。
为什么我们需要数据结构?
数据是计算机科学当中最关键的实体,而数据结构则可以将数据以某种组织形式存储,因此,数据结构的价值不言而喻。
无论你以何种方式解决何种问题,你都需要处理数据——无论是涉及员工薪水、股票价格、购物清单,还是只是简单的电话簿问题。
数据需要根据不同的场景,按照特定的格式进行存储。有很多数据结构能够满足以不同格式存储数据的需求。
常见的数据结构:
- 数组
- 栈
- 队列
- 链表
- 树
- 散列表
- 堆
- 图
1. 数组
约定存放次序:因为计算机的存储单元是一维的,数组可以是多维的,所以必须约定好存放次序。
数组是最简单、也是使用最广泛的数据结构。栈、队列等其他数据结构均由数组演变而来。
每个数据元素都关联一个正数值,我们称之为索引,它表明数组中每个元素所在的位置。大部分语言将初始索引定义为零。
数组的两种类型:
- 一维数组
- 多维数组
一维数组就是一排盒子,二维数组就是好多排盒子。
数组的基本操作
- Insert——在指定索引位置插入一个元素
- Get——返回指定索引位置的元素
- Delete——删除指定索引位置的元素
- Size——得到数组所有元素的数量
面试中关于数组的常见问题
- 寻找数组中第二小的元素
- 找到数组中第一个不重复出现的整数
- 合并两个有序数组
- 重新排列数组中的正值和负值
2. 栈
著名的撤销操作几乎遍布任意一个应用。但你有没有思考过它是如何工作的呢?这个问题的解决思路是按照将最后的状态排列在先的顺序,在内存中存储历史工作状态(当然,它会受限于一定的数量)。这没办法用数组实现。但有了栈,这就变得非常方便了。
可以把栈想象成一列垂直堆放的书。为了拿到中间的书,你需要移除放置在这上面的所有书。这就是LIFO(后进先出)的工作原理。
栈的基本操作
- Push——在顶部插入一个元素
- Pop——返回并移除栈顶元素
- isEmpty——如果栈为空,则返回true
- Top——返回顶部元素,但并不移除它
面试中关于栈的常见问题
- 使用栈计算后缀表达式
- 对栈的元素进行排序
- 判断表达式是否括号平衡
虽然我们在画的时候是把它画成竖着形状,但是实际上在我们的计算机内存中栈也是被分到一块连续的存储空间。
栈的顺序存储结构实现
栈可以使用顺序存储结构的内存空间实现,其内存空间分布如下:
栈的链式存储结构实现
栈使用链式存储结构实现的内容空间分布如下:
3. 队列
与栈相似,队列是另一种顺序存储元素的线性数据结构。栈与队列的最大差别在于栈是LIFO(后进先出),而队列是FIFO,即先进先出。
一个完美的队列现实例子:售票亭排队队伍。如果有新人加入,他需要到队尾去排队,而非队首——排在前面的人会先拿到票,然后离开队伍。
移除先入队的元素、插入新元素。
队列的顺序存储
队列的链式存储
队列的基本操作
- Enqueue()——在队列尾部插入元素
- Dequeue()——移除队列头部的元素
- isEmpty()——如果队列为空,则返回true
- Top()——返回队列的第一个元素
面试中关于队列的常见问题
- 使用队列表示栈
- 对队列的前k个元素倒序
- 使用队列生成从1到n的二进制数
4. 链表
链表是另一个重要的线性数据结构,乍一看可能有点像数组,但在内存分配、内部结构以及数据插入和删除的基本操作方面均有所不同。
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
链表就像一个节点链,其中每个节点包含着数据和指向后续节点的指针。 链表还包含一个头指针,它指向链表的第一个元素,但当列表为空时,它指向null或无具体内容。
链表一般用于实现文件系统、哈希表和邻接表。
一个节点
一个节点就像火车的一节车厢, data是车厢里面的东西, next相当于一个钩子, 用于将车厢之间的连接起来
链表初始化
定义一个节点, 其在整个链表操作中都不存储数据, 比较特殊, 与head直接相连
链表的基本操作:
插入节点有两种方式: 1是头插, 2是尾插
- InsertAtHead - 在链接列表的开头/头部插入指定元素
- InsertAtEnd - 在链表的末尾插入指定元素
- Delete - 从链接列表中删除指定元素
- DeleteAtHead - 删除链接列表的第一个元素
- Search - 从链表中返回指定元素
- isEmpty - 如果链表为空,则返回true
面试中关于链表的常见问题
- 反转链表
- 检测链表中的循环
- 返回链表倒数第N个节点
- 删除链表中的重复项
5. 树
树形结构是一种层级式的数据结构,由顶点(节点)和连接它们的边组成。 树类似于图,但区分树和图的重要特征是树中不存在环路。
树形结构被广泛应用于人工智能和复杂算法,它可以提供解决问题的有效存储机制。
树数据结构中使用的基本术语:
- Root - 根节点
- Parent - 父节点
- Child - 子节点
- Leaf - 叶子节点
- Sibling - 兄弟节点
树形结构的主要类型:
- N元树
- 平衡树
- 二叉树
- 二叉搜索树
- AVL树
- 红黑树
- 2-3树
其中,二叉树和二叉搜索树是最常用的树。
面试中关于树结构的常见问题:
- 求二叉树的高度
- 在二叉搜索树中查找第k个最大值
- 查找与根节点距离k的节点
- 在二叉树中查找给定节点的祖先节点
6. 散列表
哈希法(Hashing)是一个用于唯一标识对象并将每个对象存储在一些预先计算的唯一索引(称为“键(key)”)中的过程。因此,对象以键值对的形式存储,这些键值对的集合被称为“字典”。可以使用键搜索每个对象。基于哈希法有很多不同的数据结构,但最常用的数据结构是哈希表。
哈希表通常使用数组实现。
散列数据结构的性能取决于以下三个因素:
- 哈希函数
- 哈希表的大小
- 碰撞处理方法
面试中关于哈希结构的常见问题:
- 在数组中查找对称键值对
- 追踪遍历的完整路径
- 查找数组是否是另一个数组的子集
- 检查给定的数组是否不相交
7. 堆
8. 图
在计算机科学中,一个图就是一些顶点的集合,这些顶点通过一系列边结对(连接)。顶点用圆圈表示,边就是这些圆圈之间的连线。顶点之间通过边连接。
定义: 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为: ,其中, 表示一个图, 是图 中顶点的集合, 是图 中边的集合。
图有各种形状和大小。边可以有权重(weight),即每一条边会被分配一个正数或者负数值。考虑一个代表航线的图。各个城市就是顶点,航线就是边。那么边的权重可以是飞行时间,或者机票价格。
图的类型
- 无向图
- 有向图
- 完全图
完全图分为有向完全图和无向完全图。
无向图中的边使用小括号“()”表示,而有向图中的边使用尖括号“<>”表示。
为什么要使用图?
例如,假设你有一系列任务需要完成,但是有的任务必须等待其他任务完成后才可以开始。你可以通过非循环有向图来建立模型:
每一个顶点代表一个任务。两个任务之间的边表示目的任务必须等到源任务完成后才可以开始。比如,在任务B和任务D都完成之前,任务C不可以开始。
在程序语言中,图可以用两种形式表示:
理论上,图就是一堆顶点和边对象而已, 然后想我们怎样存储它呢,下面介绍几种存储方式。
有两种主要的方法:邻接列表和邻接矩阵。
1. 邻接矩阵
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
无向图的邻接矩阵:
我们可以设置两个数组,顶点数组为 ,边数组 为上图右边这样的一个矩阵。对于矩阵的主对角线的值,即 ,全为 是因为不存在顶点的边。
有向图的邻接矩阵:
我们再来看一个有向图样例,如下图所示的左边。顶点数组为 ,弧数组 为下图右边这样的一个矩阵。主对角线上数值依然为 。但因为是有向图,所以此矩阵并不对称,比如由 到 有弧,得到 ,而 到 没有弧,因此 。
不足:
由于存在
个顶点的图需要
个数组元素进行存储,当图为稀疏图时,使用邻接矩阵存储方法将会出现大量
元素,这会造成极大的空间浪费。这时,可以考虑使用邻接表表示法来存储图中的数据。
2.邻接表
首先,回忆我们在线性表时谈到,顺序存储结构就存在预先分配内存可能造成存储空间浪费的问题,于是引出了链式存储的结构。同样的,我们也可以考虑对边或弧使用链式存储的方式来避免空间浪费的问题。
邻接表由表头节点和表节点两部分组成,图中每个顶点均对应一个存储在数组中的表头节点。如果这个表头节点所对应的顶点存在邻接节点,则把邻接节点依次存放于表头节点所指向的单向链表中。
(1)无向图的邻接表:
从上图中我们知道,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。例如:v1顶点与v0、v2互为邻接点,则在v1的边表中,adjvex分别为v0的0和v2的2。
对于无向图来说,使用邻接表进行存储也会出现数据冗余的现象。例如上图中,顶点V0所指向的链表中存在一个指向顶点V3的同事,顶点V3所指向的链表中也会存在一个指向V0的顶点。
(2)有向图的邻接表:
若是有向图,邻接表结构是类似的,但要注意的是有向图由于有方向的。因此,有向图的邻接表分为出边表和入边表(又称逆邻接表),出边表的表节点存放的是从表头节点出发的有向边所指的尾节点;入边表的表节点存放的则是指向表头节点的某个顶点,如下图所示。
- 十字链表
- 邻接多重表
常见图遍历算法
- 广度优先搜索
- 深度优先搜索
面试中关于图的常见问题
- 实现广度和深度优先搜索
- 检查图是否为树
- 计算图的边数
- 找到两个顶点之间的最短路径