参考:visualgo
-
五种模式:链表(Linked List),栈(Stack),队列(Queue),双向链表(Double Linked List),双向队列(Double Ended Queue)。
-
动机:
-
几乎所有的计算机科学本科专业都会教链表数据结构。原因如下:
-
它是一个简单的线性数据结构。
-
做为一个抽象数据类型,它有很广泛的应用。比如,学生名单,活动清单,约会清单等(尽管还有其他更高级的数据结构可以更好地完成相同的应用程序),也可以用来实现堆栈/队列/ 双端队列。
-
有些比较特殊的情况来说明为什么需要选择一个合适的数据结构去实现你的目的。
-
它具有各种定制的方式,因此通常在面向对象编程(OOP)中来教这种链表数据结构。
-
-
列表抽象数据类型:
-
列表的顺序是非常重要的。其中位置顺序{a0, a1, ..., aN-2, aN-1 }。常见列表ADT操作为:
-
获取(i)- 也许是一个微不足道的操作,返回ai的值(基于0的索引),
-
搜索(v )-决定是否存在项/数据v(并报告其位置)或不存在(并且通常在列表中报告不存在的索引-1),
-
插入(i,v)-插入项目/数据v,在列表中的位置/索引 i,这个操作会有一个可能的问题在与他需要把索引i后面的所有数据都向后移动一位。
-
删除(i)- 删除列表中特定位置/索引i的项,这可能会让这个数据i后面的数据全部向前移动一位。
-
-
讨论:如果我们想要在列表中移除一个具体的值v,我们要如何做?
-
-
数组实现 - 第一部分
-
(紧凑)数组是实现列表ADT(抽象数据类型)的一个很好的候选对象,因为它是处理项目集合的简单结构。
-
当我们说密集阵列(compact array) 时,我们指的是一个没有间隙的数组,即如果数组中有n个项(大小m,其中m>=n),那么只有索引[0…n-1 ]的空间被占用,其他索引[n.M.1]应该保持空。
-
-
数组实现 - 第二部分
-
让紧凑数组名称为A,它的索引是[0..N-1] 的。
-
获取(i),返回A[i]。如果数组不紧凑,这个简单的操作将会不必要地复杂化。
-
搜索(v),我们逐一检查每个索引i∈ [0…n-1 ],看看是否A[i]==v。这是因为v(如果存在)可以在索引[0…n-1 ]中的任何地方。
-
插入(i,v),我们将项[i,N-1 ]移到[i+1...N](从最后面往后移动),并设置一个A[i]=v。这使得v在索引i中被正确插入并依旧保持紧凑性。
-
删除(i),我们将项 ∈ [i+1…N-1 ]移到[i, N-2],重写旧A[i]。这是为了保持紧凑。
-
-
-
时间复杂性小节
-
get(i), 非常快:只需要访问一次,O(1)。另一个CS模块:“计算机架构”会讨论了这个数组索引操作的O(1)性能的细节。
-
search(v), 在最佳情况下,v在第一位置O(1)中找到。在最坏的情况下,在列表中没有发现V,并且我们需要O(n)扫描来确定。
-
insert(i, v), 在最佳情况下插入(i,v),在i=n处插入,没有元素的移位,O(1)。在最坏的情况下,在i=0处插入,我们移动所有n个元素,O(n)。
-
remove(i), 在最佳情况下,在i=n-1处移除,没有元素的移位,O(1)。在最坏的情况下,在i=0处移除,我们移动所有n个元素,O(n)。
-
-
固定空间问题
-
密集阵列(compact array)的大小是M且不是无限的,而是有限的数。这带来了一个问题,因为在许多应用中,你可能事先不知道数组的大小。
-
如果M太大,那么闲置的空间就会被浪费掉。如果M太小,那么我们很容易耗尽空间。
-
-
变量空间
-
解决方案:使M成为变量。因此,当数组满时,我们创建一个更大的数组(通常是两倍大),并将元素从旧数组移动到新数组。因此,除了(通常很大的)物理计算机存储器大小之外,没有更多的限制。
-
然而,经典的基于数组的空间浪费和复制/转移项目的浪费仍然是有问题的。
-
-
观察:
-
对于固定大小的集合,即我们知道它有多少个元素,或者它最多有多少个元素,比如Max是M个元素,那么简单数组(array)可以被考虑去实现你的需要。
-
对于具有未知大小m的可变大小的集合,以及诸如插入/移除之类的动态操作是常见的,简单数组(array)会是一个糟糕的选择。
-
对于这样的应用,有更好的数据结构。让我们阅读…
-
-
-
链表:
-
概述:
-
现在我们介绍链表数据结构。它使用指针来允许条目/数据在内存中不连续(这是一个简单数组的主要区别)。通过将项目i与其相邻项i+1通过指针相关联,将项目从索引0排序为索引N-1。
-
-
变化:
-
链表可分为:是否有虚设头部;是否有尾指针;是否循环;
-
本人采用:
-
有虚设头部:因为头部节点中可以用来存储头指针,尾指针,数据元素的个数等有用信息。
-
有尾指针:因为在链表尾部插入数据时,非常方便。
-
不循环:循环链表的每个数据节点都需要增加一个指针域,造成空间浪费。非必要情况下不使用。
-
-
-
插入 - 四种情况:
-
由于链表的特性,它比简单数组有更多的情况。
-
大多数第一次学习链表的CS学生通常不知道所有的情况,直到他们发现他们自己的链接列表代码失败时。在这个电子讲课中,我们直接阐述所有的案例。
-
对于插入(i,v),有四个(有效)可能性,即v项被添加到:
-
头节点(在当前第一个项目之前),i=0,
-
空链表(与先前的情况类似)
-
当前链表的尾部,i=n,
-
链表的其他位置,i=〔1…n-1〕。
-
-
-
移除 - 三种情况:
-
对于remove(i),有三种可能性:
-
链表的头(当前第一项),i=0,它影响头指针。
-
链表的尾部,i=n-1,它影响尾部指针
-
链表的其他位置,i=〔1…N-2〕。
-
-
讨论:将该幻灯片与插入幻灯片进行比较,以实现细微差别。从一个空链接列表中删除任何东西是有效的操作吗?
-
-
时间负责度小节:
-
get(i)是慢的:O(n)。在链表中,我们需要执行从头部元素的顺序访问。
-
search(v)在最佳情况下,v在第一位置中找到, O(1)。在最坏的情况下,在列表中没有发现v,并且我们需要O(n)扫描来确定。
-
insert(i, v)在最佳情况下,插入i=0或i=n,在头和尾指针帮助,O(1)。在最坏的情况下,在i=n-1处插入,我们需要在找到项n-2(在尾部前的一项),O(N)。
-
删除(i)在最佳情况下,删除i=0,头指针帮助,O(1)。在最坏的情况下,在i=n-1处删除,因为需要更新尾指针O(n)。
-
-
将链表与 紧凑数组进行比较,纯(单列)链表应用程序是罕见的,因为更简单的可缩放紧凑数组(vector)可以更好地完成工作。
-
-
堆栈:
-
概述:
-
堆栈是一种特殊的抽象数据结构或者组合,其主要的(仅有的)操作为将元素加入栈中,称为进栈和将元素移除,称为出栈。堆栈是一种后进先出(LIFO-Last-In-First-Out) 的数据结构。
-
在我们的可视化中,堆栈基本上是一个受保护的单向链表,我们仅能查找栈头的元素(peek),在栈头插入新的元素(入栈)和将现有的元素从栈头移除(出栈)。
-
所有操作的时间复杂度为O(1)。
-
-
栈的应用:
-
Stack(堆栈)在教科书中有一些非常经典的应用,例如:
-
一些其他有趣的应用程序但没有用于教学目的。
-
括号匹配。
-
后缀计算器。
-
-
-
括号匹配问题:
-
数学表达式可以变得相当复杂,例如 {[x+2]^(2+5)-2}*(y+5)。
-
括号匹配问题是检查给定输入中所有括号是否正确匹配的问题,即(with)、[with ]和{with}等。
-
括号匹配对于检查源代码的合法性同样有用。
-
讨论:我们可以使用堆栈的LIFO行为来解决这个问题。那么如何解决呢?
-
-
-
队列:
-
数组实现问题 - 第一部分
-
数组实现问题 - 第二部分
-
另一种可能的数组(Array)实现是通过两个索引来避免在出列操作期间的项目转移:front(队列前端最项目)和back(队列最尾端的项目)。
-
假设我们使用大小为m=8项的数组,并且我们的队列的内容如下:[2,4,1,7,-,-,-,-] 具有前面=0和后面=3。
-
如果我们调用dequeue,我们有[-, 4, 1, 1, 7, -,-,-,-],front = 0+1=1,back=3。
-
如果我们调用enque(5),我们有[-,4,1,7,5,-,-,-],front=1,back=3+1=4。
-
-
数组实现问题 - 第三部分
-
然而,许多出列和入列操作,我们可能有[-,-,-,-,-,6,2,3], front = 5, and back = 7。到目前为止,我们不能插队任何东西,尽管我们在数组的前面有很多空的空间。
-
如果我们允许前索引和后索引在索引M-1到达索引0时“回滚”到索引,则有效地使数组“循环”,并且我们可以使用空的空间。
-
例如,如果我们调用 enqueue(8),我们有[8,-,-,-,-,6,2,3],前面=5,后面=(7+1)% 8=0。
-
-
数组实现问题 - 第四部分
-
然而,这并不能解决数组实现的主要问题:数组的项以连续的方式存储在计算机内存中。
-
稍后再进行几个入列操作,我们可能有[8,10,11,12,13,6,2,3],front=5,back=4。在这一点上,我们不能插队任何其他东西。
-
我们可以放大阵列,例如使M=2×8=16,但是这将需要在比较慢的O(n)过程中从索引前(front)到后(back)再复制项目,以具有[6,2,3,8,10,11,12,13,-,-,-,-,-,-,-],前面(front)=0,后面(back)=7。
-
-
这个时候就需要用到链表了:
-
回想一下,在队列中,我们只需要列表的两个极端,一个只用于插入(入队),一个用于删除(出列)。
-
如果我们回顾这个幻灯片,我们看到在单链接列表中尾部中插入和在头部中移除的是快非常快的,即O(1)。因此,我们将单链表的头/尾分别指定为队列的前/后。然后,因为链表中的项目没有连续存储在计算机内存中,所以我们的链表的大小可以根据需要增加和缩小。
-
在我们的可视化中,队列基本上是一个受保护的单链表,在这里我们只能查看头部项目,将一个新的项目排队到在当前尾之后的一个位置,例如尝试Enqueue(random-integer),并从头部中删除现有的项目,例如,尝试RemoveHead()(这也是出列操作)。所有的运算都是O(1)。
-
-
队列的应用
-
队列ADT通常用于模拟实际队列。
-
队列ADT的一个非常重要的应用是在 广度优先搜索图遍历算法中。
-
-
-
双端队列(Deque):
-
概述:
-
双端队列是一种抽象的数据结构,它是队列的拓展,它可以在队列的两端(头或尾)将元素加入或移除。
-
在我们的可视化中,双端队列基本上是受保护的双链表。我们仅能搜索头/尾的元素(读取头/尾),在头/尾插入新的元素(从头/尾推进),将现有头/尾元素移除(删除头/尾)。所有的操作复杂度为O(1)。
-
-
出队的应用:
-
双端队列一般用来比较高级的应用,例如在宽度优先搜素算法中找到0/1的加权图最短的路径,滑动窗口技术。
-
-
-
总结:
-
创建操作对于所有五种模式都是相同的。
-
然而,在五种模式之间的搜索/插入/移除操作存在细微差别。
-
对于堆栈,您只能从顶部/顶部查看/限制搜索,推/限制插入和弹出/限制删除。
-
对于队列,您只能从前面窥视/限制搜索,从后面推/限制插入,以及从前面弹出/限制删除。
-
对于双端队列,您可以从前/后,但不能从中间偷看/限制搜索,入队/限制插入,出队/限制删除。
-
单链表和双链表不具有这样的限制。
-