20172301 《程序设计与数据结构》第七周学习总结
教材学习内容总结
- 二叉查找树是一种含有附加属性的二叉树,其左孩子小于父结点,父结点小于或者等于右孩子。
用链表实现二叉查找树
- addElement操作:根据给定元素的值,在树中的恰当位置添加该元素。
- 判断元素是不是Comparable,不是则抛出异常。
- 树为空:新元素成为根结点。
- 树非空:新元素与根元素进行比较
- 小于:如果根的左孩子为空,成为根的左孩子;左孩子不空,遍历添加。
- 大于:如果根的右孩子为空,成为根的右孩子;右孩子不空,遍历添加。
- removeElement操作:从二叉查找树中删除给定的Comparable元素;找不到则抛出异常。
- 选择替换结点的三种情况:
(1)被删除结点没有孩子,replacement返回null;
(2)被删除结点有一个孩子,replacement返回这个孩子 ;
(3)被删除结点有两个孩子,replacement返回中序后继者;(处于根结点右子树上)
- 选择替换结点的三种情况:
- removeAllOccurrences操作:从二叉查找树中删除指定元素的所有存在。
- 方法使用了
LinkedBinaryTree
类的contains
方法。
- 方法使用了
- removeMin操作:
- 最小元素在二叉查找树的可能情况:
(1)树根没有左孩子,树根即为最小元素,树根右孩子变成新的根结点;
(2)树的最左侧结点为一片叶子,该叶子即为最小元素,设置其父结点的左孩子应用为null;
(3)树的最左侧结点为内部结点,设置其父结点的左孩子引用指向最小元素的右孩子。
- 最小元素在二叉查找树的可能情况:
用有序链表实现二叉查找树
- 树的主要使用之一就是为其他集合提供高效的实现。
操作 | 说明 | LinkedList | BinarySearchTreeList |
---|---|---|---|
removeFirst | 删除列表的第一个元素 | O(1) | O(logn) |
removeLast | 删除列表的最后一个元素 | O(n) | (logn) |
remove | 删除列表中的查找到的第一个元素 | O(n) | O(logn) |
first | 返回列表第一个元素 | O(1) | O(logn) |
last | 返回列表最后一个元素 | O(n) | O(logn) |
contains | 判断列表是否含有一个特定元素 | O(n) | O(logn) |
is Empty | 判定列表是否为空 | O(1) | O(1) |
size | 列表中的元素数目 | O(1) | O(1) |
add(有序列表特有) | 向列表添加一个元素 | O(n) | O(logn) |
- add操作和remove操作都会导致树不平衡。
平衡二叉查找树
为什么树需要平衡?
如果二叉树是一棵蜕化树,他的效率可能比线性结构还要低。也就不能满足树的高效实现。
- 右旋:指左孩子绕着其父结点向右旋转。
- 应用情况:根结点左孩子的左子树较长路径导致不平衡
- 方法步骤:
- 树根左孩子元素成为新的根元素
- 原树根元素称为新树根的右孩子元素
- 使原树根左孩子的右孩子,成为原树根的新的左孩子
- 左旋:指右孩子绕着其父结点向左旋转。
- 应用情况:根结点右孩子的右子树较长路径导致不平衡
- 方法步骤:
- 树根右孩子元素成为新的根元素
- 原树根元素称为新树根的左孩子元素
- 使原树根右孩子的左孩子,成为原树根的新的右孩子
- 左右旋:先让树根左孩子的右孩子,绕着树根的左孩子进行一次左旋,然后再让所得树根左孩子绕着树根进行一次右旋
- 应用情况:根结点左孩子的右子树较长路径导致不平衡
- 右左旋:先让树根右孩子的左孩子,绕着树根的右孩子进行一次右旋,然后再让所得树根右孩子绕着树根进行一次左旋。
- 应用情况:根结点右孩子的左子树较长路径导致不平衡
AVL树
- 平衡因子:右子树的高度减去左子树的高度。
红黑树
- 红黑树的性质:
- 树中的每一个结点都储存着红色或黑色,通常使用一个布尔值来实现,值false等价于红色。
- 根结点和叶子结点(null)为黑色。为空(null)的叶子结点才为黑色。
- 红色结点的所有孩子必定是黑色。
- 从树根到树叶的每条路径都包含有同样数目的黑色结点。
- 红黑树的平衡性质并没有AVL树那么严格,但是,他们的序仍然是logn。
教材学习中的问题和解决过程
- 问题1:关于书P228页的中序后继者的理解。
- 问题1解决方案:
- 所谓的中序后继者意思是:中序遍历二叉树结点的后继结点
- 如何查找中序后继者?
- 若右子树不为空,则找到右子树最左的叶子节点;
- 若右子树为空,且拥有右父亲节点,则找到右父亲节点;
- 若右子树为空,且拥有左父亲节点,则找到最近的右祖先节点;
- 而对于删除结点有两个孩子的情况时,不一定replacement返回中序后继者。也可以返回中继前驱者。 具体的需要看代码实现,而不需要局限于书本。
- 如何查找中序前驱者?
- 若左子树不为空,则找到左子树的最右的叶子节点;
- 若左子树为空,且拥有左父亲节点,则找到左父亲节点;
- 若左子树为空,且拥有右父亲节点,则找到最近的左父祖先节点;
- 问题2:书P228的变量
modCount
为什么是减减。 - 问题2解决方案:
- 变量
modcount
应该是计算迭代次数的。
- API里写的也是这样的,但是为什么删除操作要递减一。
- 之前实现列表的删除操作也是
modcount--
,这个未解决。
- 变量
代码调试中的问题和解决过程
-
问题1:是否需要定义新的指针类
AVLTreeNode
,换句话说,AVL树和二叉查找树以及链表实现的二叉树之间的关系。 -
问题1解决方案:
- 首先,根据书上P240所述
由于需要上溯树,因此AVL树通常最好实现为每个结点都包含一个指向其父结点的引用。
- 这里的上溯树是因为,树因为插入结点或者删除结点而变得不平衡,所以每次在进行这两个操作的时候,需要更新平衡因子,从插入或者删除的那个结点开始,检查到根结点。所以,我们的指针类很可能除了指向左右孩子的指针,还需要一个指向父结点的。
- 其次,根据书上P239所述
对于树中的每个结点,我们都会跟踪其左、右子树的高度。
- 由此,指针类会需要一个
int
型变量height
,来得出结点的高度。 - 在我实现了指针类
AVLTreeNode
和LinkedAVLTree
的平衡方法后,我需要实现添加和删除方法。但是,AVL树和二叉查找树唯一不同的是添加和删除中如果不平衡要进行旋转。 所以,AVL树是可以继承二叉查找树的。 - 这时,其实我陷入了一个思维误区。我写的指针类
AVLTreeNode
因此肯定也要继承二叉树指针类BinaryTreeNode
。但是,其实根本不用这么麻烦呀!
直接在BinaryTreeNode
构建新的构造方法不就可以了!
public BinaryTreeNode(T obj, LinkedBinaryTree<T> left, LinkedBinaryTree<T> right,int height)`
- 存在的问题:
虽然准确理解了AVL树中旋转平衡的操作,但是并没有整体理解代码与代码之间的关系。花费大量的时间做了无用功,同时让自己陷入了错误的循环。
如果,我直接发现AVL树是二叉查找树的子类,那我也不会构建新的指针类。
所以,解决代码问题,首先需要宏观的观察,确定好整体的架构,这便是UML类图的重要性。不然,尽管你细节处理的再完美,方向错了,便是越走越远。
先设计,考虑所有的情况,再去实现。
-
问题2:链表旋转方法的顺序问题。
-
问题2解决方案:这里以右旋为例。
- 根据书P238 给出右旋的操作
- 使树根的左孩子元素成为新的根元素。
- 使原根元素成为这个新树根的右孩子元素。
- 使原树根的左孩子的右孩子,成为原树根的新的左孩子。
- 所以我们实现右旋方法就可以使用一下操作,其中
node
是原树根,node1
是新树根。
node1 = node.left; node1.right = node; node.left = node1.right;
- 然后,添加上更新高度的操作。就可以返回新的根元素。
node.height = Math.max(height(node.left),height(node.right)); node1.height = Math.max(height(node1.left),height(node1.right)); return node1;
- 运行,首先给我抛出的是
StackOverflowError
错误。
- 当应用程序递归太深而发生堆栈溢出时,抛出该错误。也就是说,方法里出现了死递归。这个问题,我在上周侯泽洋同学的博客中也看见过。
- 调试发现,node的左子树是无限的,这说明右旋存在问题。
我们根据书上的操作写出来的代码,改变了node1的右子树,所以node的左子树插入的全是node,也就是无限循环的。 - 所以,我们需要改变一下操作顺序。
node1 = node.left; node.left = node1.right; node1.right = node;
这样的操作就会更加合理,同样也不会出现死递归的错误。实现代码之前,要考虑树的子树连接顺序问题,和链表类似,不要出现丢失或者赘余的情况。
-
问题3:AVL树添加/删除方法旋转情况的不全面。
-
问题3解决方案:
- AVL树旋转的原因应该是树不平衡。
- 我初次实现代码时,旋转的判断条件是当根的平衡因子绝对值大于1的时候,所以存在一种情况,即为对于根来说是平衡的,而对于根的某一结点来说是不平衡的。
- 这里我新建一个AVL数,在插入10,6,12,8,14之后,树是平衡的,并不需要平衡操作。
如图
然后,我插入数字7。
如图
这时,AVL树对于根来说是平衡的,但是对于根的左子树来说是不平衡的,因为根的左孩子没有左孩子。对于较为严格的AVL树来说,这是不符合规则的。
所以,书上给出的避免操作是建立一个指向父结点的引用,通过上溯树来判断每个结点是否是平衡的。
- 但是,我如果通过添加操作的递归在插入的时候来判断结点是否是平衡的,是否可以呢。有待实现
代码托管
上周考试错题总结
上周无错题,优秀!
结对及互评
点评过的同学博客和代码
其他
红黑树虽然难理解,但是应用的实例很多,也需要掌握。同时,AVL树相对严格,实现代码的时候要更加严谨,考虑全部的可能和情况。从而尽可能减少错误的出现。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 0/0 | 1/1 | 10/10 | |
第二周 | 610/610 | 1/2 | 20/30 | |
第三周 | 593/1230 | 1/3 | 18/48 | |
第四周 | 2011/3241 | 2/5 | 30/78 | |
第五周 | 956/4197 | 1/6 | 22/100 | |
第六周 | 2294/6491 | 2/8 | 20/120 | |
第七周 | 914/7405 | 1/9 | 20/140 |