经典数据结构深入理解(大道至简)(一)

经典数据结构深入理解(大道至简)(一)

前言

本文并不会详细的讲解各种数据结构的具体实现,而是通过尽量简洁的语句来概括这种数据结构存在的意义。

零、算法分析

算法是为求解一个问题需要遵循的、被清楚地指定的简单指令的集合。对于一个问题,一旦设计的某种算法是可以正确解决该问题的,那么接下来重要的一步就是确定该算法将需要多少诸如时间或空间资源量的问题。如果一个问题的求解算法需要长达一年时间,或者需要1G字节内存,那么这个算法恐怕也没什么实际用途。

  • 算法分析的数学基础
  1. 如果存在正常数c和n0使得当N≥n0时T(N)≤cf(N),则记为T(N)=O(f(N)),即T(N)的增长率小于等于f(N)的增长率。f(N)是T(N)的一个上界。
  2. 如果存在正常数c和n0使得当N≥n0时T(N)≥cg(N),则记为T(N)=Ω(g(N)),即T(N)的增长率大于等于g(N)的增长率。g(N)是T(N)的一个下界。
  3. T(N)=Θ(h(N))当且仅当T(N)=O(h(N))且T(N)=Ω(h(N)),即T(N)的增长率等于h(N)的增长率。
  4. 如果T(N)=O(p(N))且T(N)≠Θ(p(N)),则T(N)=o(p(N)),即T(N)的增长率小于p(N)的增长率
  • 对于一个算法,要分析的的最重要的资源一般来说就是运行时间,若无特别指定,则所需要的量T(N)是最坏情况下的运行时间。

一、链表(linked list)

链表就是为了解决数组的内存空间必须连续,而导致数据的插入和删除操作花销很大的问题而存在的一种内存空间不必连续的数据结构。

链表在C++和C等语言中是通过指针来实现。那么诸如BASIC和FORTRAN等许多语言都不支持指针,就需要用普通数组代替指针实现两个重要功能
1.每一个结构体含有数据和指向下一个结构体的方法、
2.新的结构内存的分配(malloc),和结构体内存的删除(free)操作。
这种方法称为游标(cursor)实现法

二、栈(stack)

栈是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈的顶(top)。因为该限制导致的结果,栈又叫LIFO(后进先出)表。栈基本操作有入栈和出栈两种。之所以有栈这个ADT,是因为现实中存在很多后进先出的场景,所以,抽象出这种数据类型,来高效处理这些场景。

三、队列(queue)

队列也是表,它与栈数据结构不一样,它是先进先出,即使用队列是插入在一端进行(这端叫队尾),删除在另一端进行(这端叫做队头)。队列的基本操作是入队(Enqueue)和出队(Dequeue)

四、树

对于大量的输入数据,链表的线性访问时间太慢,不宜使用。因此引出另一种数据结构—树,其大部分的运行时间平均问O(logN)。定义树的一种自然的方式是递归的方法。一棵树实质上是一些节点的集合,这个集合可以是空集,若非空,则一颗树有称做根(root)的节点r以及0个或多个非空的(子)树组成
几个定义:

  • 从节点n1到nk的路径:为节点n1,n2,…,nk的一个序列
  • 路径的长:为该路径上边的条数,如n1到nk的路径长为k-1
  • 节点ni的深度:从根到ni的唯一路径的长
  • 节点ni的高度:从节点ni到一片树叶的最长路径的长
  • 树叶:没有儿子的节点
  • 特别定义:一棵树的高度等于它的根的高度;一棵树的深度等于它的最深的树叶的深度;即一棵树的深度和高度相等。

五、二叉树

二叉树是树的一种特殊情况,其每个节点都不能有多于两个的儿子。

二叉树的应用:

  • 表达式树:树叶是操作数,其它节点是操作符。用于表达式计算。
    在这里插入图片描述
  • 二叉查找树:二叉树的一个重要作用的应用是它们在查找中的使用。使二叉树成为二叉查找树的性质是,对于树中的每个节点X,它的左子树中所有关键字值小于X的关键字值,而它的右子树中所有关键字值大于X的关键字值。这意味着,该树所有的元素可以用某种统一的方式排序。
    在这里插入图片描述
    对于二叉查找树,在没有删除或者使用懒惰删除的情况下所有操作的平均运行时间是O(logN),这也是我们期望的。但如果存在大量的删除操作,这个平均时间就不一定是O(logN)了,因为删除操作会使得左子树比右子树深度深(这是因为我们总是用右子树的一个节点来代替删除的节点),导致二叉查找树极其不平衡。那么,对应的解决办法就是要有一个称为平衡的附加结构条件,其中AVL树就是一种最老的平衡查找树。
  • AVL树:是带有平衡条件的二叉查找树。这个平衡条件必须要容易保持,而且它必须保证树的深度是O(logN)。一颗AVL树是其每个节点的左子树和右子树的高度最多差1的二叉查找树。一颗AVL树在插入节点时,如果检测到插入的节点导致AVL树的性质破坏,那么就通过单旋转或者双旋转使其再次达到平衡。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 伸展树(splay tree)
    对于二叉查找树来说,每次操作最坏情形时间O(N)并不坏,只要它相对不常发生就行。任何一次访问,即使花费O(N),仍然可能非常快。二叉查找树的问题在于,虽然一系列访问整体都有可能发生不良操作,但是很罕见。因此,累计的运行时间更重要。具有最坏运行时间O(N)但保证对任意连续操作最多花费O(MlogN)运行时间的查找树数据结构确实就可以了。
    如果任意特定操作可以有最坏时间界O(N),而我们仍然要求一个O(logN)的摊还时间,那么很清楚,只要一个节点被访问,它就必须被移动。否则,一旦我们发现一个深层的节点,我们就有可能不断对它进行Find操作。如果这个节点不改变位置,而每次访问又花费O(N),那么M次访问将花费O(M.N)的时间。
    伸展树的基本想法是,当一个节点被访问后,它就要经过一系列AVL数的旋转被放到根上。注意,如果一个节点很深,那么在其路径上就存在许多的节点也相对较深,因此,通过重新构造可以使对所有这些节点的进一步访问所花费的时间变少。

  • 树的遍历方法
    先序遍历:先处理根节点
    中序遍历:先处理左子树,再处理根节点,最后处理右子树
    后序遍历:先处理左子树,再处理右子树,最后处理根节点
    层序遍历:所有深度为D的节点要在深度为D+1的节点之前进行处理。以上三种遍历都是通过递归实现,而层序遍历并不使用递归,而是通过队列实现。

六、B-树(B-tree)

B-树是一种常用的非二叉树的查找树,阶为M的B-树是一颗具有下列结构特性的树:

  1. 树的根要么是一片树叶,要么其儿子数在2和M之间
  2. 除根外,所有非树叶节点的儿子数在[M/2]和M之间
  3. 所有的树叶都在相同的深度上,且所有的数据都存储在树叶上。

七、 散列

散列表(hash table)的实现常常叫做散列(hashing)。散列是一种用于以常数平均时间执行插入、删除和查找的技术。但是,那些需要元素间任何排序信息的操作将不会得到有效的支持。因此,诸如FindMin、FindMax以及以线性时间将排过序的整个表进行打印的操作都是散列所不支持的。

1 散列基本想法

理想的散列表数据结构只不过是一个包含有关键字的具有固定大小的数组。典型情况下,一个关键字就是一个带有相关值(例如工资信息)的字符串。表的大小记作Table-Size,并将其理解为散列数据结构的一部分而不仅仅是浮动与全局的某个变量。

每个关键字被映射到从0到TableSize-1这个范围中的某个数,并且被放到适当的单元中。这个映射就叫做散列函数(hash function),理想情况下,它应该运算简单并且应该保证任何两个不同的关键字映射到不用的单元。不过这是不可能的,因为单元的数目(也就是散列表的大小Table-Size)是有限的,而关键字实际上是无限的。因此,我们寻找一个散列函数,该函数要再单元之间均匀的分配关键字。下图是一个典型的理想情况下的散列表。john散列到3、phil散列到4、dave散列到6、mary散列到7。
在这里插入图片描述
那么,对于选择的一个散列函数,当两个关键字散列到同一个值的时候(称为冲突collision),应该做什么以及如何确定散列表的大小,这将使散列表最重要的工作。

1 散列函数

猜你喜欢

转载自blog.csdn.net/qq_34720818/article/details/109299385