算法笔记(II) 数据结构

众所周知,线性与非线性是数据结构分类的一个标准。对于线性数据结构来说,其数据的关系简单一致,因此也呈显一种表的表现形式,按照其数据的存储形式分为顺序表和链表;顺序表一般是就是我们常用的数组,因此考察数组操作是每一个计算机专业学生的基本功,数组是一种最为简单的线性结构,利于索引访问,但是不利于查找等操作。对于存储顺序并非有序的链表来说,其无法采用索引让计算机寻址操作,但是其操作灵活多变。同时,与数学一样这些线性的表可以通过多个组合使用来表现非线性的结构:树与图;当然,在最近翻看的《计算机程序的构造与解释》中,LISP语言将数据结构抽象成表结构,本质上其实现的是一种广义表形式,既可以表现线性的结构也可以是复杂的非线性结构。

同时,我们甚至数据结构与数学中的代数结构等等结构都有一定的相通之处,他们都反映了数据以及数据的关系,这种关系反应的往往是一种序关系,队和栈就是其中的典型。先入先出和先入后出,其操作表现的就是一种特殊的序关系。很多人说严蔚敏的《数据结构》一书编写的不易读,不过关于一些基本问题上却是有很多闪光的地方,例如关于各书不明确的数据结构一词有很好的说明,我觉得值得细读一下:

“ 

 数据结构(Data Structure)是相互之间存在一种或者多种特定关系的数据元素的集合。这是本书对数据结构的一种简单解释。在任何问题中,数据元素都不是孤立存在的,而是在它们之间存在着某种关系,这种数据元素相互之间的关系称之为结构(Structure)。根据数据元素之间的关系的不同特性,通常有以下四类基本结构(如篇头图):(1)集合 结构中的数据元素之间除了“同属于一个集合”的关系外,别无其他关系;(2)线性结构 结构中的数据元素之间存在着一个一对一的关系;(3)树形结构 结构中的数据元素之间存在着一个对多个的关系;(4)图状结构或网状结构 结构中的数据元素之间存在多个对多个的关系。数据结构的形式定义为:数据结构是一个二元组 Data_Strucure = (D ,S),其中:D是数据元素的有限集,S是D上关系的有限集。                                                  

 ”

数据结构的定义显然借鉴了数学上关于空间、数学结构等定义,例如空间(数学)的定义:空间是指一种具有特殊性质及一些额外结构的集合;这也让我们看到了很多科学分支往往是数学名词在新的context下的转述。下面我们将按照上述分类讲解,当然我们的大分类仍然基于线性关系以及非线性关系展开。

 

线性数据结构:队列与栈

对于队列和栈集中考察基本的实现与删除、查询、添加基本的数据结构操作上,这种线性的数据结构可以用数组(或者多个数组)实现,或者通过链表实现,一般简单的应用可以方便的利用数组实现。同时,正如在以前日志所提到的,队列与栈是两个非常有实际意义的线性数据结构,例如队列可以用于时间上(或优先级)的模拟,这一点适合于操作系统的进程管理,消息传递等等,同时将队列用于树的层次遍历,也就是宽度优先遍历;对于栈,它的意义更加的深远,从下推自动机,到递归的实现等等,都需要栈实现。同时也要明白队列的特性适合表现事件的先后次序,而栈的特性适合保存现场和回复现场,在操作系统中的中断机制中有关键的应用。

非线性数据结构:树与图

对于非线性的数据结构主要集中在 树 和 图上,本质上树是一种无环图,其存储和实现方式主要可以通过数组 和链表实现,对于数组的话可以通过两个数组完成对树的实现,一个数组记录值,另一个数组记录其父节点的索引(因为对于每一个节点其显然有不超过一个父节点);灵活的实现可以通过链表实现。

同时,考虑比树更复杂的图结构可以同样通过数组实现,如前向星或后向星就是一种树的数组存储的推广;数组实现的树结构和图结构利于编程和设计操作。

树是一种具有良好性质的数据结构,分类和用途也很广泛,其设计考虑到不同的应用:

 堆(这里考虑二叉堆),特殊的树,尤其特有的序关系;其方便提取最值,因此用于排序算法以及贪心算法;

 二叉树,其设计考虑到数的序关系是一个全序关系且具有传递性,即a<b & b<c: a<c;因此其用于查找;

  考虑二叉树这种高效的数据结构仍存在不足,如丧失平衡性的情况下,应该调整其结构使其平衡,提升效率。因此设计了很多高级的树结构:平衡二叉树,这些结构是在教科书中能看到的最复杂的数据结构,但是一般应用不会应用到,很少笔试题考到。

另外,我们考察一种独特的数据结构:串,本质上串仍然是数组,他不过是一种编码;串相关的内容仍然是考察的重点,有很多的题目关于字符串匹配以及基本处理,其中最为著名的就是字符串的匹配和查找:

如KMP算法就包含了自动机理论。如何合理的提高字符串的处理效率是一项重要内容。

图是相对最为复杂的结构,广义上图是一种对象关系模型,并注意这是一种离散结构,区别于数学分析中的连续结构。之所以提到这一点是因为,在图中定义一些性质抑或结构将区别于连续空间,例如图的距离就不可以简单的应用连续空间常用的欧式距离等等。图的算法是一般数据结构书中涉及到算是最复杂的算法,例如最小生成树、常用的最短路径算法、甚至网络流的算法。

这里提到一种特殊的图,我们称之为DAG(Directed acyclic graph),也就是有向无环图。这种图有其特殊意义,我们知道很多计算机问题往往是线性的求解模式,如线性迭代抑或线性递归。对于一些复杂的问题,往往基于分而治之的思想,拆分问题,这样一类问题往往呈现树形递归的方式。同时注意树是一种无环结构,否则将会无限的递归下去了。而因为常常会求解相同的子问题(一个节点会有若干个前驱),所以按照树的定义,这种树形结构显然并不是真正意义上的树,本质上它是有向无环图。按照我们上述的讲解,大家很容易将DAG和动态规划联系起来,事实确实是这样。

并且在编译原理中的,属性值计算中就要利用各个节点的依赖关系,它们也是用DAG表示的。

 

另类的数据结构:哈希表

同时,一个很另类的数据结构就是哈希表了,哈希表的性能依赖于哈希函数的设计,它出色的将存储内容与存储索引建立了单射的映射关系(除非发生冲突,则是多对一),这是其高效的保证。哈希技术常常用于那些需要高速的查询、修改以及插入的数据环境,如果要进行排序等功能则无法在线性时间下完成。哈希技术在计算机应用非常广泛,如文件哈希用于电驴等下载工具定位资源文件,在编译器设计中,一般利用哈希技术维护符号表,要比平衡二叉树等要方便。同时,我们的解压文件工具往往有SHA1以及CRC码校验、MD5等,事实上它们也是一种哈希函数计算得到的键值,而且他们相比发生冲突的可能性极低。

其实这里就涉及了一个问题,是不是加密算法就是一种hash呢?似乎他们都在寻求一种一对一的映射关系(其实hash只需要单射就可以了,加密必须是一一映射)。这里已经有位同学注意到了,这是他的Blog

2010-10-20

2010-11-13 补充部分内容

2011-3-8 补充Hash与加密算法的异同说明

再续

2011-3-11 修正一一映射错误,改作单射

不相交集合:并查集

并查集也是一种常用的数据结构,在实际中也有广泛的实用。其实现也相当的简单,通过一个索引表就可以完成。索引记录的是节点的父节点,如下所示:

对于两个不相交集合:

{1,2,3}   {4,5,6 } ; 我们分别以 1和4 作为这两个集合的代表元,则我们得到索引为:

index:   1,1,1,4,4,4       

在检索时,如果我们知道一个元素i与元素2同类,我们将递归的检索2的父节点直到1,并将i的索引设置为1;每种数据结构均支持不同的操作,并查集的实现结构是一种树结构,但是显然其省略了树的序关系,因此支持的操作只有三个:初始化新元素、查找元素的所属集合、合并集合;

 2010-11-24

再续

 

最近从新参看一些acm的课件,觉得其中关于数据结构的分类值得思考,正因为对于数据结构这个定义来说,仍然并没有任何确定的定义,而本文开头所述的线性与非线性的划分方式并没能涵盖繁复的数据结构,例如哈希表并不容易归类到线性或非线性的结构中去,而对于并查集这种数据结构其本质是实现包括并操作以及查找操作的集合,而树结构实现只是它的实现方式,因此单纯的将其归类到非线性结构是不可取的。而对比哈希表实现的是存储内容与键值的映射关系,所以说我们的分类方式并没有突出这个结构所蕴含的特征。

下面参见NOCOW的wiki页面,对数据结构的定义以及阐述都十分的透彻:例如在程序设计的时候,一旦确定数据结构了,往往算法也随之确定,因为算法往往是数据结构的一系列变换。而对数据结构的分类又有了相对于线性与非线性的传统分类方法的补充,即按功能分类:

按功能分类的数据结构

对于集合的实现,往往体现在如何完成查询、查找、修改、添加的功能,因此对于并查集实现集合常用的并运算以及查找元素所属类的功能;对于字典来说(字典的高效实现主要为trie以及hash两种),实现了高效的查找以及添加等等操作。对于有序表实现体现了元素间的序关系,如前驱、后驱等等,而对于树结构是父节点—子节点,兄弟节点等关系;对于hash来说,其是一种独特的集合实现,通过hash function来完成存储与内容的映射。

对于栈和队列,在按存储结构分类来说,是一种线性结构。但是这两个独特而功能强大的结构可以独立成为一种功能实现:栈用于保存现场,实现递归;队列表示优先关系,方便事件模拟;

对于优先队列(或称为堆),我觉得不应归并于传统的队列中,事实上大多数数据结构书是将优先队列独立成章的(因为传统的数据结构书是以组织结构即线性与非线性来讲解的,堆的实现往往是非线性实现方式,如二叉堆、左式堆)。但是对于按功能分类的话,则是一种广义的队列:实现优先关系。

对于双端队列来说,我认为并不方便将其孤立的列出来。双端队列(deque,全名double-ended queue)在STL中有较佳的实现,并是STL中stack、queue的基本结构(priority_queue是基于vector实现的),这三种机构均是通过适配器实现的。

后面的矩阵、串、后缀树以及剖析树等,功能过于专一:矩阵这种结构以及相关的算法在MATLAB里面就有很强的应用,串以及后缀树在字符串检索匹配等领域常用;剖析树(parse tree)主要用于编译器中的语法分析以及文档解析上。相对上面的所述结构,则用途显得窄些,在这里就不叙述了。如果需要则再补充...

用STL来理解数据结构是一个很好的方式:因为STL实现导向是应用以及功能驱动的,例如set的实现就是一个例子,尽管实现的方式是非常的复杂:红黑树,但是在用户看来不论线性与非线性也好,体现是功能的实现。

下面参见网页上的说明:

有序表的实现

优先队列的实现

的表示

2010.12.2

 

重谈stack, queue, heap(priority queue) 即栈、队列、堆(优先队列)

重新阅读了相关书籍以及stl后,我对这三种最为常用的数据结构又有了新的认识,原本打算将这段论述补充在上述关于栈与队列部分的,但是我觉得为了突出理解的过程性,我特意重点在此讲述这三种结构与搜索方法上的关联[wiki]。

我们知道分而治之是一种重要的算法设计思路,广泛应用于多种算法中,如分治法,动态规划等等,不同的搜索方式将决定使用何种数据结构进行辅助,反过来说,使用何种数据结构也就决定了我们的算法。例如使用stack将伴随着DFS以及回溯,queue将对应于宽度优先搜索,而对于heap或者优先队列来说,其伴随着分治定界算法(B&B)[hudong baike]。不同的搜索方法维护不同的数据结构,也就造成了不同的搜索模式。

分支定界算法与回溯不同:

回溯使用DFS方式遍历解答树,是一种穷举法的巧妙实现(因为一定程度上避免扩展根本不可能的分支);分支定界的实现方式主要是宽度优先或者带有启发性的优先搜索方式,因此可以维护一个队列结构或者一个优先队列结构。

回溯法搜索解答树时,不免要搜索本质上其实相同的情况(即解答树上的节点)多次,但是在分支定界算法中,每一个节点只被扩展一次,对于子节点来说,我们会通过限界法舍弃那些不好的情况甚至不可行的情况。设想我们每次采用一个堆结构获得当前最有可能会是最优解的节点优先扩展它,那我们似乎更有可能早一点搜索最优解。

2010.12.05

附记 2011-5-19:

在 回溯算法 和 DFS  的异同上存在争议。回溯搜索似乎本身就是一种DFS,但是正如《数据结构与算法分析》中所言,回溯是一种有技巧的穷举。在《人工智能——一种现代方法》中,作者精辟地提到: 回溯是DFS的一种变形,其使用的内存空间比DFS更少。原因有两点:

  •  只记录当前扩展的节点和下一个待扩展的节点,而不是记录当前层所有待扩展的节点,这样内存就下降了一个 分支数倍数b;
  •  通过修改状态描述,恢复状态描述来辅助搜索,将状态描述压缩为只有一个,行动数目仍然是最大深度m。(很自然是一个压栈和出栈,但是dancing link 给了我们新的启发) 

自己的解释能力有限,参见原文:

A variant of depth-first search called backtracking search uses still less memory. In backtracking, only one successor is generated at a time rather than all successors; each partially expanded node remembers which successor to generate next. In this way, only O(m) memory is needed rather than O(bm). Backtracking search facilitates yet another memory-saving (and time-saving) trick: the idea of generating a suc-cessor by modifying the current state description directly rather than copying it first. This reduces the memory requirements to just one state description and O(m) actions.

For this to work, we must be able to undo each modification when we go back to generate the next successor. For problems with large state descriptions, such as robotic assembly, these techniques are critical to success.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章