【算法】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-08-25 11:26:55

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

算法复杂度,及三种主要排序算法的研究

一.时间复杂度 1.时间频度  T(n),n为问题的规模 即--算法中语句的执行次数.又叫语句频度. 2.时间复杂度 记作 O( f(n) ),这里的f(n)是一个T(n)的同数量级函数. 如O(1)表示算法的语句执行次数为一个常数,不随规模n的增长而增长: 又如T(n)=n^2+3n+4与T(n)=4n^2+2n+1它们的频度不同, 但时间复杂度相同,都为O(n^2). 3.算法的性能 主要用算法的 时间复杂度 的数量级来评价一个算法的时间性能. 二.空间复杂度 S(n),包括3方面: 1.算

白话经典算法系列之一 冒泡排序的三种实现

分类: 白话经典算法系列 2011-08-06 19:20 93923人阅读 评论(72) 收藏 举报 算法优化 冒泡排序是非常容易理解和实现,,以从小到大排序举例: 设数组长度为N. 1.比较相邻的前后二个数据,如果前面数据大于后面的数据,就将二个数据交换. 2.这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就“沉”到数组第N-1个位置. 3.N=N-1,如果N不为0就重复前面二步,否则排序完成. 按照定义很容易写出代码: [cpp] view plaincopy //冒泡

白话经典算法系列之一 冒泡排序的三种实现 【转】

冒泡排序是非常容易理解和实现,,以从小到大排序举例: 设数组长度为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; i++) for (

css不定高图文垂直居中的三种方法

html部分 <div class="box"> <img class="img" src="http://p2.so.qhmsg.com/bdr/_240_/t0196d3945287174d27.jpg" alt=""> <span class="text">111111111</span> </div> css部分 /*方法1:table-c

链表的三种创建形式

刚刚学习完链表,总结了三种链表的创建方式,从表前插入节点,从表后插入节点和它的进化版?? #include <stdio.h> #include <stdlib.h> typedef struct node { char data; struct node *next; }linkList; //下面??的是从前面插入,但是缺点比较明显,因为链表的顺序和你输入的顺序是相反的...推荐使用后面的 linkList *CreatList_1() { char ch; linkList

【算法】8 链表及其哨兵是如何支撑起这种优雅的数据结构

三种链表的介绍 原谅我拙劣的绘图能力,花了半天终于还是决定从网上找来了这三张图,因为环形链表的弧形箭头难以完美的展现出来. 以下3张图片来自Wikipedia. 大家看着图片应该也都知道这分别是哪种链表了.那么链表到底是什么呢? 它和前面的栈和队列一般,都是基本的数据结构,其中的各个对象按线性顺序排列.大家应该注意到了图中的大黑点,有些C/C++编程基础的同学肯定能够猜到链表是通过各个对象里的指针来指向下一个对象的,相比,数组则是通过下标来进行索引. 为了让大家加深印象,我们来联系到生活中的实例

图解排序算法(一)之3种简单排序(选择、冒泡、直接插入)

先定义个交换数组元素的函数,供排序时调用 /** * 交换数组元素 * @param arr * @param a * @param b */ public static void swap(int []arr,int a,int b){ arr[a] = arr[a]+arr[b]; arr[b] = arr[a]-arr[b]; arr[a] = arr[a]-arr[b]; } 简单选择排序(O(n^2)) 简单选择排序是最简单直观的一种算法. 基本思想:每一趟从待排序的数据元素中选择最小

reids过期键三种删除策略

redis设计与实现(第二版) 过期键删除策略 ? 我们知道数据库的过期时间都保存在过期字典中,又知道了如何根据过期时间去判断一个键是否过期,现在的问题是:如果一个键过期了,那么它什么时候会被删除呢? ? 这个问题有三种可能的答案,它们分别代表三种不同的 删除策略: 定时删除:在设置键的过期时间的同时,创建一个定时器(timer)让定时器在键的过期时间来临时,立即执行对键的删除操作 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键:如果没有过

FS获取KERNEL32基址的三种方法

FS寄存器指向当前活动线程的TEB结构(线程结构) 偏移  说明 000  指向SEH链指针 004  线程堆栈顶部 008  线程堆栈底部 00C  SubSystemTib 010  FiberData 014  ArbitraryUserPointer 018  FS段寄存器在内存中的镜像地址 020  进程PID 024  线程ID 02C  指向线程局部存储指针 030  PEB结构地址(进程结构) 034  上个错误号 在shellcode中用它来找KERNEL32.DLL基地址是常