java基础之数据结构

一、概念

1、算法复杂度:在给定输入规模时,为获得最终的结果而需要执行的基本操作数量。

2、渐近记号(Asymptotic Notation)通常有 O、 Θ 和 Ω 记号法。Θ 记号渐进地给出了一个函数的上界和下界,当只有渐近上界时使用 O 记号,当只有渐近下界时使用 Ω 记号。尽管技术上 Θ 记号较为准确,但通常仍然使用 O 记号表示。

复杂度标记符号描述
常量(Constant)

 O(1) 

操作的数量为常数,与输入的数据的规模无关。

n = 1,000,000 -> 1-2 operations 

对数(Logarithmic)

 O(log2 n) 

操作的数量与输入数据的规模 n 的比例是 log2 (n)。

n = 1,000,000 -> 30 operations

线性(Linear) O(n)

操作的数量与输入数据的规模 n 成正比。

n = 10,000 -> 5000 operations

平方(Quadratic) O(n2)

操作的数量与输入数据的规模 n 的比例为二次平方。

n = 500 -> 250,000 operations

立方(Cubic) O(n3)

操作的数量与输入数据的规模 n 的比例为三次方。

n = 200 -> 8,000,000 operations

指数(Exponential)

 O(2n)

 O(kn)

 O(n!)

指数级的操作,快速的增长。

n = 20 -> 1048576 operations

注意:在算法导论中,采用记号 lg n = log2 n ,也就是以 2 为底的对数。

3、树中经常使用的术语

根(Root):树中最顶端的节点,根没有父节点。
子节点(Child):节点所拥有子树的根节点称为该节点的子节点。
父节点(Parent):如果节点拥有子节点,则该节点为子节点的父节点。
兄弟节点(Sibling):与节点拥有相同父节点的节点。
子孙节点(Descendant):节点向下路径上可达的节点。
叶节点(Leaf):没有子节点的节点。
内节点(Internal Node):至少有一个子节点的节点。
度(Degree):节点拥有子树的数量。
边(Edge):两个节点中间的链接。
路径(Path):从节点到子孙节点过程中的边和节点所组成的序列。
层级(Level):根为 Level 0 层,根的子节点为 Level 1 层,以此类推。
高度(Height)/深度(Depth):树中层的数量。比如只有 Level 0,Level 1,Level 2 则高度为 3。

二、树结构分类以及园林分析

1、二叉树

二叉树是一种特殊的树形结构,每一个节点最多只有左右两个子节点,

分类:完全二叉树和满二叉树

完全二叉树(Complete Binary Tree):深度为 h,有 h 个节点的二叉树,当且仅当其每一个节点都与深度为 h 的满二叉树中,序号为 1 至 n 的节点对应时,称之为完全二叉树;简单的讲就是它的每一层,除了可能的最后一个,都是完全填满的,所有的节点都尽可能地向左移动。
满二叉树(Full Binary Tree):一棵深度为 h,且有 2h - 1 个节点称之为满二叉树;简单的讲就是每一个节点都有两个孩子。

                                              

 完全二叉树满二叉树
  总节点数 k    2h-1 <= k < 2h - 1  

 k = 2h - 1

  树高 h   h = log2k + 1

 h = log2(k + 1)

       二叉树的数据存放并不像数组那样是线性的存放。如果要访问二叉树中的某一个节点,通常需要逐个遍历二叉树中的节点,来定位那个节点。它不象数组那样能对指定的节点进行直接的访问。所以查找二叉树的渐进时间是线性的 O(n),在最坏的情况下需要查找树中所有的节点。也就是说,随着二叉树节点数量增加时,查找任一节点的步骤数量也将相应地增加。
       那么,如果一个二叉树的查找时间是线性的,定位时间也是线性的,那相比数组来说到底哪里有优势呢?毕竟数组的查找时间虽然是线性 O(n),但定位时间却是常量 O(1) 啊?的确是这样,通常来说普通的二叉树确实不能提供比数组更好的性能。然而,如果我们按照一定的规则来组织排列二叉树中的元素时,就可以很大程度地改善查询时间和定位时间。

二叉查找树(BST:Binary Search Tree)是一种特殊的二叉树,也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),它改善了二叉树节点查找的效率。二叉查找树有以下性质:
对于任意一个节点 n,
其左子树(left subtree)下的每个后代节点(descendant node)的值都小于节点 n 的值;
其右子树(right subtree)下的每个后代节点的值都大于节点 n 的值。

下图中展示了两个二叉树。二叉树(b)是一个二叉查找树(BST),它符合二叉查找树的性质规定。而二叉树(a),则不是二叉查找树。因为节点 10 的右孩子节点 8 小于节点 10,但却出现在节点 10 的右子树中。同样,节点 8 的右孩子节点 4 小于节点 8,但出现在了它的右子树中。无论是在哪个位置,只要不符合二叉查找树的性质规定,就不是二叉查找树。例如,节点 9 的左子树只能包含值小于节点 9 的节点,也就是 8 和 4。

                                        

       从二叉查找树的性质可知,BST 各节点存储的数据必须能够与其他的节点进行比较。给定任意两个节点,BST 必须能够判断这两个节点的值是小于、大于还是等于。
       假设我们要查找 BST 中的某一个节点。例如在上图中的二叉查找树(b)中,我们要查找值为 10 的节点。
       我们从根开始查找。可以看到,根节点的值为 7,小于我们要查找的节点值 10。因此,如果节点 10 存在,必然存在于其右子树中,所以应该跳到节点 11 继续查找。此时,节点值 10 小于节点 11 的值,则节点 10 必然存在于节点 11 的左子树中。在查找节点 11 的左孩子,此时我们已经找到了目标节点 10,定位于此。
       如果我们要查找的节点在树中不存在呢?例如,我们要查找节点 9。重复上述操作,直到到达节点 10,它大于节点 9,那么如果节点 9 存在,必然存在于节点 10 的左子树中。然而我们看到节点 10 根本就没有左孩子,因此节点 9 在树中不存在。

总结来说,我们使用的查找算法过程如下:
        假设我们要查找节点 n,从 BST 的根节点开始。算法不断地比较节点值的大小直到找到该节点,或者判定不存在。每一步我们都要处理两个节点:树中的一个节点,称为节点 c,和要查找的节点 n,然后并比较 c 和 n 的值。开始时,节点 c 为 BST 的根节点。然后执行以下步骤:
如果 c 值为空,则 n 不在 BST 中;
比较 c 和 n 的值;
如果值相同,则找到了指定节点 n;
如果 n 的值小于 c,那么如果 n 存在,必然在 c 的左子树中。回到第 1 步,将 c 的左孩子作为 c;
如果 n 的值大于 c,那么如果 n 存在,必然在 c 的右子树中。回到第 1 步,将 c 的右孩子作为 c;
通过 BST 查找节点,理想情况下我们需要检查的节点数可以减半。如下图中的 BST 树,包含了 15 个节点。从根节点开始执行查找算法,第一次比较决定我们是移向左子树还是右子树。对于任意一种情况,一旦执行这一步,我们需要访问的节点数就减少了一半,从 15 降到了 7。同样,下一步访问的节点也减少了一半,从 7 降到了 3,以此类推。


根据这一特点,查找算法的时间复杂度应该是 O(log­2n),简写为 O(lg n)。可知,log­2n = y,相当于 2y = n。即,如果节点数量增加 n,查找时间只缓慢地增加到 log­2n。下图中显示了 O(log­2n) 和线性增长 O(n) 的增长率之间的区别。时间复杂度为 O(log­2n) 的算法运行时间为下面那条线。

                                       

从上图可以看出,O(log­2n) 曲线几乎是水平的,随着 n 值的增加,曲线增长十分缓慢。举例来说,查找一个具有 1000 个元素的数组,需要查询 1000 个元素,而查找一个具有 1000 个元素的 BST 树,仅需查询不到10 个节点(log21024 = 10)。

BST 算法查找时间依赖于树的拓扑结构。最佳情况是 O(log­2n),而最坏情况是 O(n)。

2、红黑树

红黑树,本质上来说就是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(lg n)。

但它是如何保证一棵n个结点的红黑树的高度始终保持在h = lgn的呢?这就引出了红黑树的5条性质:

1)每个结点要么是红的,要么是黑的。  
2)根结点是黑的。  
3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。  
4)如果一个结点是红的,那么它的俩个儿子都是黑的。  
5)对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。  
红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的5条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。

左旋


对x进行左旋,意味着"将x变成一个左节点"。

左旋的伪代码《算法导论》:参考上面的示意图和下面的伪代码,理解“红黑树T的节点x进行左旋”是如何进行的。

复制代码
LEFT-ROTATE(T, x)  
01  y ← right[x]            // 前提:这里假设x的右孩子为y。下面开始正式操作
02  right[x] ← left[y]      // 将 “y的左孩子” 设为 “x的右孩子”,即 将β设为x的右孩子
03  p[left[y]] ← x          // 将 “x” 设为 “y的左孩子的父亲”,即 将β的父亲设为x
04  p[y] ← p[x]             // 将 “x的父亲” 设为 “y的父亲”
05  if p[x] = nil[T]       
06  then root[T] ← y                 // 情况1:如果 “x的父亲” 是空节点,则将y设为根节点
07  else if x = left[p[x]]  
08            then left[p[x]] ← y    // 情况2:如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
09            else right[p[x]] ← y   // 情况3:(x是它父节点的右孩子) 将y设为“x的父节点的右孩子”
10  left[y] ← x             // 将 “x” 设为 “y的左孩子”
11  p[x] ← y                // 将 “x的父节点” 设为 “y”
复制代码

理解左旋之后,看看下面一个更鲜明的例子。你可以先不看右边的结果,自己尝试一下。


右旋

对x进行左旋,意味着"将x变成一个左节点"。

右旋的伪代码《算法导论》:参考上面的示意图和下面的伪代码,理解“红黑树T的节点y进行右旋”是如何进行的。 

复制代码
RIGHT-ROTATE(T, y)  
01  x ← left[y]             // 前提:这里假设y的左孩子为x。下面开始正式操作
02  left[y] ← right[x]      // 将 “x的右孩子” 设为 “y的左孩子”,即 将β设为y的左孩子
03  p[right[x]] ← y         // 将 “y” 设为 “x的右孩子的父亲”,即 将β的父亲设为y
04  p[x] ← p[y]             // 将 “y的父亲” 设为 “x的父亲”
05  if p[y] = nil[T]       
06  then root[T] ← x                 // 情况1:如果 “y的父亲” 是空节点,则将x设为根节点
07  else if y = right[p[y]]  
08            then right[p[y]] ← x   // 情况2:如果 y是它父节点的右孩子,则将x设为“y的父节点的左孩子”
09            else left[p[y]] ← x    // 情况3:(y是它父节点的左孩子) 将x设为“y的父节点的左孩子”
10  right[x] ← y            // 将 “y” 设为 “x的右孩子”
11  p[y] ← x                // 将 “y的父节点” 设为 “x”
复制代码

理解右旋之后,看看下面一个更鲜明的例子。你可以先不看右边的结果,自己尝试一下。

区分 左旋 和 右旋

仔细观察上面"左旋"和"右旋"的示意图。我们能清晰的发现,它们是对称的。无论是左旋还是右旋,被旋转的树,在旋转前是二叉查找树,并且旋转之后仍然是一颗二叉查找树。


左旋示例图(以x为节点进行左旋):

                               z
   x                          /                  
  / \      --(左旋)-->       x
 y   z                      /
                           y

对x进行左旋,意味着,将“x的右孩子”设为“x的父亲节点”;即,将 x变成了一个左节点(x成了为z的左孩子)!。 因此,左旋中的“左”,意味着“被旋转的节点将变成一个左节点”

右旋示例图(以x为节点进行右旋):

                               y
   x                            \                 
  / \      --(右旋)-->           x
 y   z                            \
                                   z

对x进行右旋,意味着,将“x的左孩子”设为“x的父亲节点”;即,将 x变成了一个右节点(x成了为y的右孩子)! 因此,右旋中的“右”,意味着“被旋转的节点将变成一个右节点”

参考资料:

https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/03.01.md

http://www.cnblogs.com/skywang12345/p/3245399.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值