数据结构简要遍览

来源:互联网 发布:vwap 算法交易 编辑:程序博客网 时间:2024/04/27 20:53

数据结构者,数据的存储方式也,不同的存储方式决定了数据之间不同的关系,不同的关系决定了在这组数据上执行各种操作的复杂度。数据结构问题就是在问题面前选择合适的结构来存储问题数据,从而比较高效地完成要求的操作。


下面简要介绍各种经典数据结构的存储思想(即数据间关系)、实现方式和所擅长的操作。

 

第一层次数据结构:

 

第一层次数据结构有“语元”的感觉,是实现复杂数据结构的基础,其本身也有一些很好的应用。

 

一、线性表(Linear List)

存储思想:线性表是最简单最直接的数据结构,是一列前后有序的元素集合。
抽象数据类型:应支持随机查找、查找、插入、删除、查找有序表等主要操作
实现方式:最简单的线性表也有多达4种实现方式,每种实现方式代表一种组织数据的思想,都会在后面更复杂的数据结构中使用。
 1.连续存储。用一块连续的内存即一个数组存放。
 随机查找的复杂度为O(1),查找、插入和删除的复杂度均为O(n),利用二分查找查找有序表可以达到O(logn)。
 2.链式存储。用一串相继指向下一节点的节点串存放。
 随机查找的复杂度上升为O(n),查找复杂度为O(n),插入和删除操作由于链表的作用降低为O(1),而查找有序表也提高为O(n)——因为链表在随机查找方面很无奈。
 3.间接寻址。一种稍显繁琐的存储方式,是连续存储和链式存储的结合。用一块连续的内存存放一个指针数组,每个指针负责跟踪对应的线性表元素。
 各项操作的复杂度和连续存储基本相同,实际思想还是连续存储,只不过加了一层指针而已。随机查找复杂度保持为O(1),插入和删除保持为O(n),只是由于间接寻址的关系,插入和删除时不需移动实际元素,而只需移动指针元素,但个人认为本质上没有区别,这种存储方式意义不大。
 4.模拟指针。用整型值代替指针,不直接指出下一元素的内存位置而是指出下一元素的数组索引。空间利用率较低。
 各项操作的复杂度和链式存储相同。
用途:
 1.其它高级数据结构的底层形式。
 2.桶排序:桶排序是一种应用范围较苛刻的排序方式,复杂度可以达到O(n)。可以应用在待排序数据范围很小的情况,思想是维护n个桶(用线性表实现)代表数据范围内的n个值(k——k+n-1),遍历待排序数据将各个数据放在对应桶中,再从一号桶开始依次取出各桶元素得到有序数列。
 3.基数排序:是对桶排序的一种改进,用m次入桶、m次回收达到排序目的,每次排列一个数位,从个位开始到最高位,这样在进行了第i次入桶、回收后,数据的倒数i位就是有序的了,直到最后所有位有序,完成排序。
 4.凸包中也用到线性表。

 

二、栈(Stack)

存储思想:栈和下面要谈到的队列都是对操作进行限制后的线性表。
抽象数据类型:应支持入栈(在栈顶插入)、出栈(从栈顶删除)等主要操作。
实现方式:
 连续存储和链式存储两种,由于栈所支持的操作较少,因此两种存储方式在操作复杂度上相差不大。
用途:
 1.栈最重要的应用就是深度优先搜索(DFS,Depth First Search)了。典型问题如迷宫问题。用栈来放置已经走过的路径,当无法继续前进时弹出栈顶元素后退一步,继续搜索其他道路。深度优先搜索是一种求解“是否有解”的算法,要求出最优解还需要广度优先搜索(BFS)。
 2.括号匹配。
 3.汉诺塔问题。

 

三、队列(Queue)

存储思想:和栈一样,是对线性表的操作进行限制而得来的。
抽象数据类型:应支持入队(在队尾插入)、出队(在队头删除)等主要操作。
实现方式:
 连续存储和链式存储两种,由于队列所支持的操作较少,因此两种存储方式在操作复杂度上相差不大。
 由于队列的插入和删除在不同端进行,若采用普通连续存储方式会造成队头方向大量空间浪费,因此应使用循环队列。
用途:
 最重要的用途自然是广度优先搜索(BFS,Breadth First Search)。用队列来放置还没有走过的节点,每走过一个节点将该节点的后续节点入队并将当前节点出队,当搜索成功或队列为空(搜索失败)时结束搜索。


四、树

存储思想:树描述数据之间的层次关系。许多后续的针对特殊应用的数据结构(如并查集)都可以用树来实现。
实现方式:由于树的结构非常自由,每个节点可以有任意多个孩子,因此能有效存储树的实现方式并不多。典型的有两种:二叉树表示和父节点表示法。
 1.二叉树表示法。将树转换为二叉树进行存储。
 2.父节点表示法。这是一种比较简单但使用起来很有限制的实现方法。用一块连续内存(数组)存储树的所有节点,每个节点除data域外还有一个parent域,根节点的parent域为0,其余各节点的parent域标示该节点的父节点索引。这样从任何一个节点都可以追溯到其所有祖先一直到根节点,但是反之则不行。并查集就是用这种方式来实现的。
用途:
 并查集(Union Find Set),用于解决在线等价类问题。用父节点表示法的树来实现。实现过程中还有路径压缩等优化手段。

 

五、二叉树

存储思想:二叉树是非常重要的数据结构,很多后续的针对特殊应用的数据结构(堆、搜索树等)都可以以二叉树和树为基础。二叉树由于具有严谨的结构,因此有很多很好的性质,特别是完全二叉树。普通二叉树也可以利用完全二叉树的各种性质,只要把对应节点看做空即可。
抽象数据类型:应支持前序、中序、后序、层次四种遍历方式,以及计算层高等主要操作。
实现方式:实现方式主要有两种:公式化描述和链表实现。
 1.公式化描述。把二叉树看作缺少了一部分节点的完全二叉树,用一块连续的内存(数组)存放。可以利用完全二叉树各节点之间的对应关系方便进行各种操作。
 2.链式存储。链式存储又分为二叉链表和三叉链表。应用较为广泛的是二叉链表,即每个二叉树节点有data、leftchild、rightchild三个域,若再加上parent域,则为三叉链表。
用途:
 二叉树的用途主要是以之为基础实现其它特殊的二叉树,如堆、左高树、竞赛树、搜索树等。

六、图

 

第二层次数据结构:

 

第二层次数据结构是用第一层次数据结构实现的、针对各种实际应用而构造出的较复杂的数据结构。

 

一、字典(Dictionary)

设计目的:对于一个连续存储的有序表,搜索某关键字元素的复杂度为O(logn),若是链式存储,则需要O(n),因为有序对于链式存储是一种无法利用的条件。为了改善这种按值(关键字)查找的低效率,提出“字典”数据结构。
抽象数据类型:字典要实现的是按值存取。即快速地按值查找和配套的插入、删除操作。
实现方式:字典的实现方式有跳表、散列。
 1.跳表。用链式存储实现,给出多级链表,使得在一个有序链表中可以跳过不需要查看的元素,即保留了链式存储的优点(不需在插入和删除操作中移动元素),又在一定程度上克服了链式存储在随机访问上的困难。但是实现和操作较复杂而且效率不算高,按值查找的复杂度为O(logn),基于查找的插入和删除自然也为O(logn)。
 2.散列(Hash)。散列即所谓的哈希,在值和存储位置之间建立多对一的映射关系——即所谓哈希函数。由于是多对一,自然要丢失信息,散列后再恢复为有序是很困难的。但是散列可以将按值查找和基于此的插入、删除的复杂度降低为O(1)。由于哈希函数是多对一的映射,除了丢失顺序信息外还有一个问题就是可能产生冲突。解决冲突的方式有两种:开放寻址和链式存储。较好的方式是链式存储,可由STL vector很好地支持。
用途:
 一个很典型的应用是“判重”。判断一个集合中有没有重复元素,因为相同元素经过哈希函数必然映射到同一个位置,而接下来的比较就很简单了,比如若是链式存储解决冲突的话,就可以沿链比对。基于“判重”的问题有很多,都可以通过散列解决。也可以直接使用STL map,STL map提供的也是一种按值存取,STL map是用红黑树实现的,各项操作效率是O(logn)。

 

二、优先队列(Priority Queue)

设计目的:优先队列和队列有些相似,两者的读取和删除操作都只能在指定点进行,队列必须在队头进行,而优先队列则必须在最值处(优先级最高处)进行。是一种能够快速给出或者删除数据集合中的最大值(或最小值)的数据结构。实现思想是在数据集合中保持一种有序状态,这样可以在O(1)的复杂度下给出集合中的最值,至于删除操作,由于删除后还需要将优先队列重构从而继续保持优先队列的性质,所以要麻烦一些,可接受的复杂度为O(logn)。
实现方式:有两种典型的实现方式:堆实现和左高树实现。两者都是二叉树,数据结构中大多数O(logn)的复杂度都来自二叉树的结构性质,一个例外是有序线性表的二分查找。左高树比堆得要求苛刻,所以其能实现的操作也自然多于堆实现。
抽象数据类型:应实现查找最值、删除最值和插入等重要操作。
 1.堆实现。堆是完全二叉树,优先队列可以用最大堆(任何一个节点都大于其左右孩子,左右子树之间的节点大小没有限制)或者最小堆实现。最大堆的查找复杂度为O(1),插入和删除的复杂度是O(logn),这是由重构最大堆的操作复杂度决定的。
最大堆的插入和删除思想很有意思,插入时将插入节点放入完全二叉树的末尾,然后向上比较交换直到形成最大堆;删除操作用最后一个节点覆盖根节点,再向下比较交换直到形成最大堆。优先队列的最大堆实现直观、简单,但是无法执行两个优先队列合并的操作,遇到这种操作要求就要求助于左高树实现。
 2.左高树实现。左高树是这样一棵二叉树,从任意一个节点看,左子树的高度都高于右子树。最大左高树(每个节点都大于其左右孩子的左高树)可用来实现优先队列。最大左高树优于最大堆的地方就是它可以快速实现两棵最大左高树的合并,复杂度为O(logn)。最大左高树的插入和删除也是通过合并操作来实现的:插入可以看做是将原最大左高树和新插入节点所代表的最大左高树进行合并;删除可以看做根节点的左右子树进行合并。
用途:
 堆排序。

 

三、竞赛树

设计目的:是优先队列的一种延续,基本操作也是返回数据集合中的最值,当最值被替换后通过重新比赛再次得到新的最值。
实现方式:竞赛树有两类——胜者树和败者树,都是完全二叉树,因此可以高效地用公式化描述进行实现。

 

四、搜索树

设计目的:在用散列实现的字典中,虽然插入、搜索和删除都可以在O(1)内完成,但却不能执行一些按照关键字顺序执行的操作,如:按序输出、查找第k个元素、删除第k个元素等。用跳表可以实现这些操作,但既繁琐又没有理想的最坏情况复杂度。二差搜索树,特别是平衡搜索树可以很好地满足这些要求。平衡搜索树可在O(n)内完成按序输出,并在O(logn)内完成查找第k个元素和删除第k个元素。
实现方式:搜索树是一棵二叉树,因此可用二叉树的两种实现方式来实现。
抽象数据类型:二叉搜索树(BSTree)中每个节点的关键字值各不相同,而且每个节点的关键字值都大于左孩子、小于右孩子,因此从左到右看去,应该得到一个升序序列。应提供插入、查找、删除和按序输出等主要操作。带索引的二叉搜索树(IndexBSTree)还提供按序查找和按序删除操作。所谓带索引,就是在每个节点添加一个leftsize域,值为左子树的元素个数加一。leftsize值表示在这个以该节点为根的二叉树中该节点的序列号。
操作实现:
 1.按序输出。由于每个节点都满足左孩子<节点<右孩子,因此对该二叉树进行中序遍历就可以按序输出。复杂度为O(n)。
 2.搜索。很简单,搜索树就是干这个用的,类似有序表的二分查找。从根节点开始对比,用比较结果指导接下来该在左子树还是右子树进行比较。复杂度为O(h)。
 3.按序查找。与搜索类似,从根节点开始对比leftsize值,用比较结果指导接下来该在左子树还是右子树进行比较,每次拐向右子树都将待查找值k减去当前节点的leftsize值。复杂度为O(h)。
 4.插入。也是基于搜索,先搜索欲插入的元素,如果搜索成功说明原本就包含这个元素,由于搜索树不能包含重复元素,故返回。如果搜索不成功也到了该插入此节点的叶子节点处了,通过比较确定应该将新节点作为叶子节点的左孩子还是右孩子。
 5.删除。删除比较复杂,首先也是搜索该节点,若搜索失败则是BadInput,若搜索成功又分为三种情况:1)欲删除元素是叶子节点,则直接删除之;2)该元素只有一个非空子树,这种情况下,如该节点又是根节点,则直接让根节点指向其子树即可,若不是根节点,则修改其父节点的指针指向其子树;3)该元素既有左子树又有右子树,就是用该节点左子树中的最大元素或者右子树中的最小元素代替它,再删除用来代替它的这个元素原来所在的节点,这时要利用搜索树的这样一个性质,左子树的最大节点在左子树的最右边,右子树的最小节点在右子树的最左边,因此无论是左子树的最大节点还是右子树的最小节点都不可能再是既有左子树又有右子树的节点了,代替后的删除可以按照1)和2)进行。
 6.按序删除。既然能按序查找,查找之后自然能删除之。
由于搜索树的很多操作复杂度都依赖于高度h,因此我们希望高度h尽可能的小,高度为logn(最优)的二叉树称为平衡树。比较典型的平衡树有AVL树和红黑树。平衡搜索树既是搜索树又是平衡树,能更好地满足搜索树的操作要求。
(一)AVL树
定义:空二叉树是一棵AVL树;对于非空二叉树,任一节点的左右子树高度差小于等于1,即所有叶子节点分布在最下面两层里。只要给AVL树加上索引就可以实现按序操作了。