【算法】8 图文搭配诠释三种链表及其哨兵

三种链表的介绍

原谅我拙劣的绘图能力,花了半天终于还是决定从网上找来了这三张图,因为环形链表的弧形箭头难以完美的展现出来。

以下3张图片来自Wikipedia。

大家看着图片应该也都知道这分别是哪种链表了。那么链表到底是什么呢?

它和前面的栈和队列一般,都是基本的数据结构,其中的各个对象按线性顺序排列。大家应该注意到了图中的大黑点,有些C/C++编程基础的同学肯定能够猜到链表是通过各个对象里的指针来指向下一个对象的,相比,数组则是通过下标来进行索引。

为了让大家加深印象,我们来联系到生活中的实例。

首先是单向链表(singly linked),我第一个联想到的就是下面这种铅笔,满满的儿时回忆呀!找了好久才找到这张图,却不知道它的名字。

然后是双向链表(doublely linked list),动车组则可以很好的诠释它。

循环链表(circular linked list)的应用是比较多的,从小接触的自行车链条就是其中之一。

大家要是还有什么例子欢迎在评论中留下哦。

链表是如何指引的

单链表

前面已经说到了,链表通过指针来指向下一个对象。单链表中有一个关键字key和指针next,当然了,对象中还可以有其他的卫星数据。我们可以这样想象它,前面的图中是一行对吧,然后在行中的链表节点中向下延伸,每个节点都延伸成一列,简单的说,从一维变成了二维(类比二维数组)。

将链表中的一个元素设为x,那么x.key就是它的值,x.next就是链表中的后继元素。如果x.next=NIL,那么就说明没有后继元素了,因此x就是链表的尾(tail)。

双向链表

将单链表升级到双向链表来考虑,无非就是多了一个前驱,用x.prev来表示。同样的,x.prev=NIL,表示没有前驱,那么x就是链表的头(head)。而如果头都为空了,那么整个链表也就是空的了。

循环链表

相应的,循环链表也由双向链表升级而来,就是将链表尾部的元素x的next指向链表的头部y,元素头部的元素y的prev指向链表的尾部x。

链表的搜索、插入、删除

搜索

我们的目的是要搜索出链表L中第一个关键字为k的元素,函数返回的将是指向该元素的指针。

如果不幸的是链表中不存在这个元素,那么就返回NIL。

LIST-SEARCH(L,k)
1   x=L.head
2   while x!=NIL and x.key!=k
3       x=x.next
4   return x

由于这个搜索是线性的,在最坏的情况下它会搜索整个链表,因此该情况下LIST-SEARCH的运行时间为Θ(n)。

循环

接下来我们将元素x(已经设置好关键字key)插入到链表中,这个相比搜索就有些复杂,因为它要修改的东西较多一些。L.head.prev的意思是去链表的头节点元素,然后取它的prev属性。

LIST-INSERT(L,x)
1   x.next=L.head
2   if L.head!=NIL
3       L.head.prev=x
4   L.head=x
5   x.prev=NIL

它仅仅是在开头插入一个元素而已,因此耗时仅仅是Θ(1)。

删除

我们有了一个指向x的指针,然后要将x从列表中删除掉。具体的思路也非常的简单,例如有依次链接的A、B、C三个节点,如果要将B删除掉,只需要将A的next指向C即可,如果是双线链表也请记得将C的prev指向A。

LIST-DELETE(L,x)
1   if x.prev!=NIL
2       x.prev.next=x.next
3   else L.head=x.next
4   if x.next!=NIL
5       x.next.prev=x.prev

由于这里的x已经是指针了,因此删除操作只需要Θ(1)的时间,而如果给定的不是指针而是关键字,那么就要调用LIST-SEARCH先搜索到指针x,这样的话时间就是Θ(n)。

哨兵

今天我忽然觉得在博客上多加点图片,即便是现在这个“哨兵”图像,虽然和链表没太大关系,但也许可以帮助记忆呢,因为记忆真的非常非常重要。

废话不多说,哨兵是什么呢,能够做什么呢?

哨兵节点常常被用在链表和遍历树中,它并不拥有或引用任何被数据结构管理的数据。常常用哨兵节点来代替null,这样的好处有以下3点:

1)增加操作的速度
2)降低算法的复杂性和代码的大小
3)增加数据结构的鲁棒性

补充:鲁棒性(robustness)是指的稳健性或稳定性,也就是说,当某个事物受到干扰时,这个东西的性质依旧稳定。网上有一个例子,在统计中,均值受到极端值的影响可谓非常之大,而在这种情况下中位数就要稳定得多。

补充:还有一个哨兵值的定义(也被称为标志值、信号值和哑值),它是在特定算法中的一个特殊值,常用它来让条件终止,由此可见它被普遍用于循环和递归之中。

简而言之,哨兵就是为了简化边界条件的处理而存在。回头看看链表的删除过程,用了两个if来判断,而用了哨兵值就大可不必这么麻烦。

LIST-DELETE'(L,x)
1   x.prev.next=x.next
2   x.next.prev=x.prev

既然是哨兵了,那么它站岗的位置自然也是在边界了,对于链表而言,那就是头部和尾部之间。

图片上下的3个箭头请大家自行脑补成一个箭头。

在有哨兵之前,我们必须通过L.head来访问表头,现在可以通过L.nil.next来访问表头了。

L.nil就是守卫链表疆土的哨兵,那么L.nil.prev就自然的指向表尾了,相应的L.nil.prev指向表头。

上面已经对删除做了修改,下面也来看看搜索和插入。

搜索

相比删除而言,搜索中原本就对边界的使用不多,此处只需将第一行的L.head换成L.nil.next和将NIL换成L.nil即可。

LIST-SEARCH'(L,k)
1   x=L.nil.next
2   while x!=L.nil and x.key!=k
3       x=x.next
4   return x

插入

和删除一样,边界的判断再也不需要了!

LIST-INSERT'(L,x)
1   x.next=L.nil.next
2   L.nil.next.prev=x
3   L.nil.next=x
4   x.prev=L.nil

哨兵的作用和注意事项

通过上面有无哨兵的3个操作也可以看出来,哨兵并没有减少算法的渐进时间界,不过可以降低常数因子,例如LIST-DELETE’和LIST-INSERT’都节约了O(1)。当然,在某些情况下,哨兵能够降低的更多。但它更多的作用是在于使代码更加简洁和紧凑。

然而哨兵也需要慎用,正所谓”是药三分毒”,如果存在很多的短小链表,那么再给每一个链表配上一个哨兵就不划算了,因为哨兵要占用额外的存储空间,而短小的年表很多时,就造成了严重的浪费。



那么这篇博客就到此为止咯,最近都在考试,算法系列更新的比较少,不过依旧感谢大家对我的支持!

时间: 2024-07-29 21:09:01

【算法】8 图文搭配诠释三种链表及其哨兵的相关文章

经典算法(2) 直接插入排序的三种实现

直接插入排序(Insertion Sort)的基本思想是:每次将一个待排序的记录,按其关键字大小插入到前面已 经排好序的子序列中的适当位置,直到全部记录插入完成为止. 设数组为a[0-n-1]. 1. 初 始时,a[0]自成1个有序区,无序区为a[1..n-1].令i=1 2. 将a[i]并入当前的有序区a[0-i-1]中形 成a[0-i]的有序区间. 3. i++并重复第二步直到i==n-1.排序完成. 下面给出严格按照定 义书写的代码(由小到大排序): void Insertsort1(in

LinkedList和List在三种简单算法中效率比较

.Net 框架提供了两种List类型,一种是基于链表的LinkedList, 一种是基于数组的List.那么在实际应用中到底采用哪种List,如何取舍呢?本文对两种类型在队列,堆栈和简单插入三种简单算法中的效率进行了一个比较. 首先先让我们来看一下List的初始容量Capacity对List的性能是否有影响. 测试方法:分别设置初始容量为0,64,255,1024. List插入的最大长度为1000,循环1000次,得到如下结果,单位为ms,下同. 算法/初始容量 0 64 255 1024 队

JavaScript中数据结构与算法(三):链表

  这篇文章主要介绍了JavaScript中数据结构与算法(三):链表,本文分别讲解了单链表与双链表以及增加节和删除节的代码实例,需要的朋友可以参考下 我们可以看到在javascript概念中的队列与栈都是一种特殊的线性表的结构,也是一种比较简单的基于数组的顺序存储结构.由于javascript的解释器针对数组都做了直接的优化,不会存在在很多编程语言中数组固定长度的问题(当数组填满后再添加就比较困难了,包括添加删除,都是需要把数组中所有的元素全部都变换位置的,javascript的的数组确实直接

搜索引擎常用的三种网站排序算法

搜索引擎如何对互联网上那么多的网站进行合适的排名?想必做站长的都想知道这一点,这是通过一套非常繁琐复杂的算法计算出来的,具体的算法想必没有几个人知道,但是最常用的三种算法还是需要大家去了解一下的. 1.词频位置加权排序算法:顾名思义是说从整个网站上的文字的位置上与出现的次数进行排序,先来说一下位置,不同的网站关键词在内容里出现与在标题里面出现时差别非常大的,搜索引擎认为标题能表现出一个网站是干什么的,如果标题里面出现了关键词要远比文章里面出现关键词重要的多的多.这就是现在大家都知道一个网站的标题

从三种算法简要分析外链的重要性

福清SEO对于搜索引擎算法从没有深入研究过,也不提倡去深入研究.笔者认为一般的企业SEOer只需要对算法有所了解,浅尝辄止就可以了. 下文福清SEO列举三种著名搜索引擎算法进行初入门分析,不为别的,就是为证明外链对于网站排名的重要性提供理论依据,希望对大家有帮助. 关于Hits算法 Hits算法是在1997年由康奈尔大学博士提出的,是针对一个页面重要性分析的算法.Hits算法有两个概念,分别是枢纽值与权威值. 枢纽值的含义就是该页面所有导出链接的权值和. 比如:福清SEO博客导出链接20个,这2

企业站点三种关键词协调搭配提高转化率

  当我们选好关键词后,往往开始对关键词进行相应的布局.而关键词布局也非常的简单,主页选主关键词,比如企业站的主页关键词一般是公司名称和主要产品.分类目录选择次关键词,比如公司的产品是疏水阀,次关键词基本是这种产品的具体类型,像蒸汽疏水阀这样次关键词.而内容页关键词就是我们熟知的长尾关键词,它的存在是对产品的补充说明,比如述说产品的功能,怎么使用这种类型.长尾关键词有效的引来访客,增加用户购买的基数.三种关键词协调搭配,在站点提高PV,IP的同时,也有利用提高UV,即我们所说的转化率. 一.主关

三种中文分词算法优劣比较

到目前为止,中文分词包括三种方法:1)基于字符串匹配的分词:2)基于理解的分词:3)基于统计的分词.到目前为止,还无法证明哪一种方法更准确,每种方法都有自己的利弊,有强项也有致命弱点,简单的对比见下表所示: 各种分词方法的优劣对比 分词方法 基于字符串匹配分词 基于理解的分词 基于统计的分词 歧义识别 差 强 强 新词识别 差 强 强 需要词典 需要 不需要 不需要 需要语料库 否 否 是 需要规则库 否 是 否 算法复杂性 容易 很难 一般 技术成熟度 成熟 不成熟 成熟 实施难度 容易 很难

经典算法(1) 冒泡排序的三种实现

冒泡排序是非常容易理解和实现,,以从小到大排序举例: 设数组长度为N. 1.比较相邻 的前后二个数据,如果前面数据大于后面的数据,就将二个数据交换. 2.这样对数组的第0个数据到 N-1个数据进行一次遍历后,最大的一个数据就"沉"到数组第N-1个位置. 3.N=N-1,如果N不为0就 重复前面二步,否则排序完成. 按照定义很容易写出代码: //冒泡排序1 void BubbleSort1(int a[], int n) { int i, j; for (i = 0; i < n;

算法系列(六)最长公共子序列(LCS)问题(连续子序列)的三种解法

最长公共子序列(LCS)问题有两种方式定义子序列,一种是子序列不要求不连续,一种是子序列 必须连续.上一章介绍了用两种算法解决子序列不要求连续的最终公共子序列问题,本章将介绍要求 子序列必须是连续的情况下如何用算法解决最长公共子序列问题. 仍以上一章的两个字符串 "abcdea"和"aebcda"为例,如果子序列不要求连续,其最长公共子序列为"abcda",如果子序列 要求是连续,则其最长公共子序列应为"bcd".在这种情况下