参考自两篇文章
原文链接:
https://baijiahao.baidu.com/s?id=1609200503642486098&wfr=spider&for=pc
原文链接:https://blog.csdn.net/yeyazhishang/article/details/82353846
什么是数据结构?
简单地说,数据结构是以某种特定的布局方式存储数据的容器。这种“布局方式”决定了数据结构对于某些操作是高效的,而对于其他操作则是低效的。首先我们需要理解各种数据结构,才能在处理实际问题时选取最合适的数据结构。
为什么需要数据结构?
数据是计算机科学当中最关键的实体,而数据结构则可以将数据以某种组织形式存储,因此,数据结构的价值不言而喻。
无论你以何种方式解决何种问题,你都需要处理数据——无论是涉及员工薪水、股票价格、购物清单,还是只是简单的电话簿问题。
数据需要根据不同的场景,按照特定的格式进行存储。有很多数据结构能够满足以不同格式存储数据的需求。
常见的数据结构
- 数组
- 栈
- 队列
- 链表
- 树
- 图
- 字典树(这是一种高效地树形结构,值得单独说明)
- 散列表(哈希表)
1、数组
数组是最简单、也是使用最广泛的数据结构。栈、队列等其他数据结构均由数组演变而来。
每个数据元素都关联一个正数值,即为索引,它表明数组中每个元素所在的位置。大部分语言将初始索引定义为零,可以通过索引对数组元素进行访问。
int[] data = new int[10];
data[0] = 0;
以下是数组的两种类型:
- 一位数组
- 多维数组(数组的数组)
优点:
- 按照索引查询元素速度快;
- 按照索引遍历数组很方便。
缺点:
- 数组的大小固定后不能扩容;
- 数组只能存储同一种类型的数据;
- 添加、删除操作慢,因为要移动其他的元素。
适用场景:
- 频繁查询,对存储空间要求不大,很少增加或删除的情况。
数组的基本操作:
- Insert()——在指定索引位置插入一个元素;
- Get()——返回指定索引位置的元素;
- Delete()——删除指定索引位置的元素;
- Size()——得到数组所有元素的数量。
面试中关于数组的常见问题:
- 寻找数组中第二小的元素;
- 找到数组中第一个不重复出现的整数;
- 合并两个有序数组;
- 重新排列数组中的正值和负值。
2、栈
著名的撤销操作几乎遍布任意一个应用。但你有没有思考过它是如何工作的呢?这个问题的解决思路是按照将最后的状态排列在先的顺序,在内存中存储历史工作状态(当然,它会受限于一定的数量)。这没办法用数组实现。但有了栈,这就变得非常方便了。
栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。 栈的特点是:先进后出,或者说是后进先出,从栈顶放入元素的操作叫入栈,取出元素叫出栈。 栈的结构就像一个集装箱,越先放进去的东西越晚才能拿出来,所以,栈常应用于实现递归功能方面的场景,例如斐波那契数列。
【补充:裴波那契数列】
斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……
在数学上,斐波纳契数列以如下被以递推的方法定义:
F
(
1
)
=
1
,
F
(
2
)
=
1
,
F
(
n
)
=
F
(
n
−
1
)
+
F
(
n
−
2
)
(
n
>
=
3
,
n
∈
N
∗
)
F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N^*)
F(1)=1,F(2)=1,F(n)=F(n−1)+F(n−2)(n>=3,n∈N∗)
栈的基本操作
- Push()——在顶部插入一个元素;
- Pop()——返回并移除栈顶元素;
- isEmpty()——如果栈为空,则返回true;
- Top()——返回顶部元素,但并不移除它。
面试中关于栈的常见问题
- 使用栈计算后缀表达式;
- 对栈的元素进行排序;
- 判断表达式是否括号平衡。
3、队列
栈相似,队列是另一种顺序存储元素的线性数据结构。栈与队列的最大差别在于栈是LIFO(后进先出),而队列是FIFO(先进先出)。
一个完美的队列现实例子:售票亭排队队伍。如果有新人加入,他需要到队尾去排队,而非队首——排在前面的人会先拿到票,然后离开队伍。
适用场景:
多线程阻塞队列管理。
队列的基本操作
- Enqueue()——在队列尾部插入元素;
- Dequeue()——移除队列头部的元素;
- isEmpty()——如果队列为空,则返回true;
- Top()——返回队列的第一个元素。
面试中关于队列的常见问题
- 使用队列表示栈;
- 对队列的前k个元素倒序;
- 使用队列生成从1到n的二进制数。
4、链表
链表是物理存储单元上非连续的、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,每个元素包含两个结点,一个是存储元素的数据域 (内存空间),另一个是指向下一个结点地址的指针域。 链表还包含一个头指针,它指向链表的第一个元素,但当列表为空时,它指向null或无具体内容。根据指针的指向,链表能形成不同的结构,链表包括以下类型:
- 单链表
- 循环链表
- 双向链表
链表的优点:
- 链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;
- 添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快。
缺点:
- 因为含有大量的指针域,占用空间较大;
- 查找元素需要遍历链表来查找,非常耗时。
适用场景:
- 数据量较小,需要频繁增加,删除操作的场景。链表一般用于实现文件系统、哈希表和邻接表。
链表的基本操作:
- InsertAtEnd()——在链表的末尾插入指定元素;
- InsertAtHead()——在链接列表的开头/头部插入指定元素;
- Delete()——从链接列表中删除指定元素;
- DeleteAtHead()——删除链接列表的第一个元素;
- Search()——从链表中返回指定元素;
- isEmpty()——如果链表为空,则返回true。
面试中关于链表的常见问题
- 反转链表;
- 检测链表中的循环;
- 返回链表倒数第N个节点;
- 删除链表中的重复项。
5、树
树形结构是一种层级式的数据结构,由顶点(节点)和连接它们的边组成。 树类似于图,但区分树和图的重要特征是树中不存在环路。树形结构被广泛应用于人工智能和复杂算法,它可以提供解决问题的有效存储机制。它具有以下特点:
- 每个节点有零个或多个子节点;
- 没有父节点的节点称为根节点;
- 每一个非根节点有且只有一个父节点;
- 除了根节点外,每个子节点可以分为多个不相交的子树。
树形结构的基本术语:
- Root - 根节点;
- Parent - 父节点;
- Child - 子节点;
- Leaf - 叶子节点;
- Sibling - 兄弟节点。
树形结构的主要类型:
- N元树;
- 平衡树;
- 二叉树;
- 二叉搜索树;
- AVL树;
- 红黑树;
- 2-3树。
其中,二叉树和二叉搜索树是最常用的树。它具有以下几个特点:
- 每个结点最多有两颗子树,结点的度最大为2;
- 左子树和右子树是有顺序的,次序不能颠倒;
- 即使某结点只有一个子树,也要区分左右子树。
面试中关于树结构的常见问题:
- 求二叉树的高度;
- 在二叉搜索树中查找第k个最大值;
- 查找与根节点距离k的节点;
- 在二叉树中查找给定节点的祖先节点。
6、图
图是由结点的有穷集合V和边的集合E组成。其中,为了与树形结构加以区别,在图结构中常常将结点称为顶点,边是顶点的有序偶对,若两个顶点之间存在一条边,就表示这两个顶点具有相邻关系。按照顶点指向的方向可分为无向图和有向图:
在程序语言中,图可以用两种形式表示
- 邻接矩阵;
- 邻接表。
常见图遍历算法
- 广度优先搜索;
- 深度优先搜索。
面试中关于图的常见问题
- 实现广度和深度优先搜索;
- 检查图是否为树;
- 计算图的边数;
- 找到两个顶点之间的最短路径。
7、字典树
字典树,也称为“前缀树”,是一种特殊的树状数据结构,对于解决字符串相关问题非常有效。它能够提供快速检索,主要用于搜索字典中的单词,在搜索引擎中自动提供建议,甚至被用于IP的路由。
以下是在字典树中存储三个单词“top”,“thus”和“their”的例子:
这些单词以顶部到底部的方式存储,其中绿色节点“p”,“s”和“r”分别表示“top”,“thus”和“theirs”的底部。
面试中关于字典树的常见问题
- 计算字典树中的总单词数;
- 打印存储在字典树中的所有单词;
- 使用字典树对数组的元素进行排序;
- 使用字典树从字典中形成单词;
- 构建T9字典(字典树+ DFS )。
8、散列表(哈希表)
散列表,也叫哈希表,是根据关键码和值 (key和value) 直接进行访问的数据结构,通过key和value来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。
记录的存储位置=f(key)
这里的对应关系 f 称为散列函数,又称为哈希 (hash函数),而散列表就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里,这种存储空间可以充分利用数组的查找优势来查找元素,所以查找的速度很快。
哈希表在应用中也是比较常见的,就如Java中有些集合类就是借鉴了哈希原理构造的,例如HashMap,HashTable等,利用hash表的优势,对于集合的查找元素时非常方便的,然而,因为哈希表是基于数组衍生的数据结构,在添加删除元素方面是比较慢的,所以很多时候需要用到一种数组链表来做,也就是拉链法。拉链法是数组结合链表的一种结构,较早前的hashMap底层的存储就是采用这种结构,直到jdk1.8之后才换成了数组加红黑树的结构,其示例图如下:
从图中可以看出,左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
哈希表的应用场景很多,当然也有很多问题要考虑,比如哈希冲突的问题,如果处理的不好会浪费大量的时间,导致应用崩溃。
面试中关于哈希结构的常见问题:
- 在数组中查找对称键值对;
- 追踪遍历的完整路径;
- 查找数组是否是另一个数组的子集;
- 检查给定的数组是否不相交。