8种基本数据结构介绍
作为程序员,我们对数据结构的理解大多局限于用一种编程语言在较高的抽象层次上使用它们。虽然我们知道如何使用特定的编程语言从不同的数据结构中存储和检索数据,但我们中的大多数人并没有尝试去解开这些数据结构的低级实现中的内容。
在大多数情况下,数据结构的表面知识足以让我们的工作顺利完成。但是,当涉及到为给定的任务选择最佳的数据结构时,了解不同的数据结构在较低层次上的行为是至关重要的。在这篇文章中,我们将看看8种不同的数据结构的包装下,看看它们是如何处理数据的。
阵列
数组数据结构存储了固定数量的单一数据类型的数据。数组中的元素(项目)被存储在一个连续的内存插槽块中。因此,数组中的元素被分配了连续的数字,从0或1开始,作为其 "索引"。
人们可以使用其唯一的索引随机访问存储在数组中的单个元素。使用索引访问一个元素的时间复杂性为Θ(1)。读取或更新数组元素可以通过这种方式轻松实现。由于数组元素的连续位置,与其他大多数数据结构相比,数组的遍历更快。
在数组中插入或删除是一项相当复杂和耗时的任务。当插入时,当前数组中的所有元素被复制到一个新创建的数组中,并增加其大小,新元素被添加到新数组的末端。删除也是以类似的方式实现的,以减少数组的大小。
应用。
数组可以是多维的(数组的数组)。这使得数组成为存储矩阵和向量的良好选择。数组经常被用来实现其他数据结构,如列表、堆、栈和队列。
队列
队列数据结构类似于我们在日常生活中看到的队列:第一个进入队列的人是第一个获得下一个出队机会的人。在编程世界的队列版本中,每一个添加到队列中的新数据元素都存储在后端,每一个从队列中移除的元素都从前端取出--先入先出。
队列操作
- Enqueue:将一个元素插入队列的末端。新加入的元素成为队列的后端元素。
- Dequeue:从队列的前部移除一个元素。enqueue和dequeue操作的时间复杂性都是Θ(1)。
- Peek: 读取队列前面的元素,而不删除或修改它。
应用
队列被用来实现缓冲区。多线程使用队列来管理等待被线程执行的任务。
堆栈
堆栈与队列很相似,但它们是在后进先出的基础上实现的,而不是在先进先出的基础上。想象一下,在一摞菜中,最后添加的菜是第一个要被移走的菜。
堆栈操作
- 推:在堆栈的顶部插入一个新的元素。新添加的元素成为新的顶层元素。
- 弹出。将一个元素从堆栈的顶部移除。push和pop操作的时间复杂度都是Θ(1)。
- Peek: 读取堆栈顶部的元素而不删除或修改它。
应用
堆栈被用来处理和评估数学表达式。它们也被用于使用回溯程序的算法中。在递归编程中处理递归函数调用是另一种应用。
链接列表
链接列表是一种动态数据结构。这意味着存储在链接列表中的数据项的数量可以轻易地扩大或缩小。与具有固定大小的数组相比,这使链接列表具有更大的灵活性。链接列表通过将每个项目作为一个单独的对象来存储,实现了这种动态特性。
链接列表中的元素不必存储在连续的内存槽中,相反,每个元素(称为节点)存储一个指向下一个节点位置的指针。这些指针维持着与链表中独立节点的联系。除了指向下一个节点的指针外,一个节点还存储一个数据字段。
在链表中有两个重要的节点:头和尾。
- 头:链表的第一个节点。
- 尾:链表的最后一个节点。尾部的指针值被设置为空。
当向链表插入一个新元素时,新的数据字段被存储在内存中的一个特定位置,前面节点中的指针被更新以指向新节点。新节点存储先前存储在前一个节点的指针。
当删除一个节点时,被删除的节点前面的节点被赋予以前存储在被删除节点中的指针。
然而,对于链接列表,如果不从头开始遍历列表,你就不能直接访问一个数据项。这使得访问操作的时间复杂度为Θ(n)。
链接列表的类型
- 单一的链接列表。上面的例子中显示的关联列表都是单链式列表。一个节点只包含一个指向下一个节点的指针。
- 双链式列表。双链表中的一个节点包含指向给定节点之前和之后的节点的指针。列表的遍历可以在前向和后向方向进行。
- 循环链表。尾部的指针指向头部,而不是空的。从本质上讲,循环链表没有尾巴,只有一个头。
应用
链接列表被用来实现数据结构,如堆栈、队列和图形。在进行多项式代数操作时,链接列表被用来存储常数。
图
一个图由有限数量的数据项组成,称为顶点(V)。这些顶点中的一些对通过边(E)相互连接。由一条边连接的两个顶点彼此相邻。
图可以用不同的属性进行分类。这种分类之一是有向图和无向图。
- 在有向图中,连接两个顶点的边有一个起始顶点和一个终止顶点。当穿越图形时,该边缘只能从起点顶点穿越到终点顶点。
-
在无向图中,一条边可以不受限制地在两个方向上进行穿越。
应用
像Facebook这样的社交媒体应用程序使用图来表示其用户为顶点,其友谊为边。谷歌网页排名算法使用图来表示网页和连接它们的链接。谷歌地图使用图来表示其运输系统中的道路网络。
二叉树
二叉树与有向图有一些相似之处。两者之间的区别是,在二叉树中,数据被存储在一个层次结构中,上层的节点被称为父节点,下层的节点被称为子节点。二叉树中的一个节点只能有一个父节点,最多有两个子节点。
让我们来看看与二叉树相关的几个术语。
- 根:在树的顶端的节点。它没有父节点。
- 叶子:树底的一个节点。它没有子节点。
- 键:存储在一个节点中的数据值。
- 子树:由一个节点的所有子节点组成的树。
有许多特殊的二进制树,如二进制搜索树,Treap,二进制尝试,和Heap。
二进制搜索树
二进制搜索树按照排序的顺序存储数据值。在二进制搜索树中,一个节点的左边子节点的数值必须小于父节点,右边子节点的数值必须大于父节点。
顾名思义,BST的主要优点是能够快速搜索存储的数据。在BST中搜索一个存储元素的时间复杂度是O(log n)。
应用
- 二进制搜索树被用来实现编程语言中的map和set对象
- 二进制尝试用于存储路由器表。
- 树被用在无线网络中。
堆
堆是二进制树的另一个特例。在堆中,根的键与它的子的键相比较,以特定的方式安排它们。有两种类型的堆。
- 最大堆:父节点的键大于或等于子节点的键。根节点存储给定数据集中的最大值。
- 最小堆:父方的键小于或等于子方的键。根节点存储给定数据集中的最小值。
考虑到我们得到了整数值(33, 24, 45, 12, 90, 78, 23, 53)作为数据集。我们可以从这个数据中构建一个单独的最大堆和最小堆。
最小堆
最大堆
在堆中插入、删除和提取最大(或最小)函数的时间复杂度为O(log n)。但是找到最大(或最小)的时间复杂度只有O(1)。
应用
堆被用于实现堆排序算法。堆也被用来实现优先级队列,因为堆的第一个元素总是存储着具有最大(或最小)优先级的值。
哈希表
当我们想在大数据集上保持搜索和插入操作的速度时,哈希表是我们可以使用的最有效的数据结构之一。存储在散列表中的每个数据值都与一个键相关联,如果我们知道这个键,就可以快速访问存储的值。想想一个学生注册系统,每个学生都有一个唯一的学生ID,这个ID可以作为一个键来存储他们在哈希表中的数据。
哈希表使用数组来存储数据值。键是用来寻找数组中存储值的索引。但是,哈希表是如何将这些键与它们的值进行映射的呢?
可以使用的方法之一是直接寻址。它使用一对一的映射:每个键都指向其数据所存储的确切位置。但这种方法不能有效地使用内存,特别是当键值对的数量增加,键的大小变大时。因此,取而代之的是,哈希表使用哈希函数。
哈希函数
哈希表使用哈希函数将数据值映射到它们的键上。它将键值的范围转换为数组索引的范围。通过将键传递给哈希函数而产生的索引或值被称为哈希值。下面是一个哈希函数的例子。
h(k) = k % m
- h是哈希函数
- h(k)是对应于键k的哈希值
- k是密钥
- m是哈希表的大小。m的一个好选择是一个不接近2的幂的质数。
让我们考虑几个键的哈希值。考虑m=20。
- k=1001, h(k) = 1001%20 = 1
- k=1055, h(k) = 105520 = 15
- k=20123,h(k)=20123%20=3
对于k值,1001,1055和20123,它们的相关值分别存储在哈希表的索引1,15和3。
考虑键2021的哈希值,它是1。我们之前看到,与键1001相关的值被存储在哈希表的索引1处。当两个键产生的哈希值相似时,我们称其为碰撞。哈希表使用链式和开放寻址等技术来解决这种碰撞问题。
哈希表的搜索和插入时间复杂度为O(1)。
应用
哈希表被用来实现数据库索引。编译器使用哈希表来识别编程语言中的关键词。计算机使用哈希表来连接文件名和它们的路径。
总结
这篇文章对我们作为程序员每天与之打交道的8种数据结构的底层逻辑做了基本介绍。有了这些关于不同数据结构的独特属性的知识,从今天起,你在为你的编程任务选择最合适的数据结构时可以更加注意。但请记住,这只是一个基本介绍。关于数据结构,你可以而且应该学习的东西还有很多。