茶艺师学算法打卡11:二叉树
学习笔记
二叉树
二叉树是树(tree)的一种,具体来说是最多只有左子节点与右子节点的树。
除了叶子节点,所有节点都有左子节点与右子节点,这二叉树就是满二叉树。
叶子节点分布在最底一层和倒数第二层,且最底一层的叶子节点都靠左排列,这样的树是完全二叉树。
【图 满二叉树 完全二叉树】
完全二叉树为何完全,要看看二叉树如何存。
通用的存法是用链表法,如:
type treeNode struct {
val int
left *treeNode
right *treeNode
}
但是完全二叉树与满二叉树能用数组存,如:
树的根存在数组下标“1”的位置,对于一个下标为 x 的节点 i,它其他节点可以通过计算下标得出:
- 下标 2x 处存的是 i 的左子节点
- 下标 2x + 1 处存的是 i 的右子节点
- 下标 x/2 处存的是 i 的父节点
这里就能理解为什么“完全”了,完全二叉树存成数组,就是一个不会有空的数组。
二叉树有不少遍历方法,最耳熟能详的就是这么三个:前序遍历,中序遍历,后序遍历。
其中的“序”,是指根节点什么时候遍历。
// 前序遍历
func perCal(root *treeNode) {
if root == nil {
return
}
print(root.Val) // 遍历根
perCal(root.Left)
perCal(root.Right)
}
// 中序遍历
func midCal(root *treeNode) {
if root == nil {
return
}
midCal(root.Left)
print(root.Val) // 遍历根
midCal(root.Right)
}
// 后序遍历
func posCal(root *treeNode) {
if root == nil {
return
}
posCal(root.Left)
posCal(root.Right)
print(root.Val) // 遍历根
}
在代码上也能很直观得看出根是在什么时候遍历,而且这样的遍历也很适合用递归来写。
二叉搜索树
二叉搜索树是二叉树的一种,它的特点是,某节点 i ,与左右子节点 left 、 right ,要满足这样的关系: 。
因此有着这样的特性:当用中序遍历一个二叉搜索树时,就能得到一个有序序列。
二叉搜索树被设计出来是为了快速查找,实际上它也支持快速插入、删除数据。其操作的简单思路是:
- 查找 先比较根的值,如果比根的值大,就递归查找右子树,否则就递归查找左子树
- 插入 先把值插入叶子节点,然后比较它的根的值。如果比根小,且根没有左子节点,就直接挪到那里去,不然就递归遍历它的左子树;反之亦然。
- 删除 这就有点麻烦,得考虑三种情况:要删除的点没有子节点、有一个子节点、有两个子节点。没有子节点的,直接删掉就是了;有一个子节点的,就把要删除节点的父节点连到要删除节点的子节点,再把这个节点删掉就行了;有两个子节点的,先找到待删除节点的右子树里的最小节点,两者替换,再把**替换后的“最小节点(待删除节点)”**删掉就行了。
对二叉搜索树稍微改造一下,也能支持存同样数值。改造思路有二:
- 思路1 用链表法存一对数据
- 思路2 每个节点仍存一个数据,但是遇到相同的数据,直接存到这个节点的右边,即
如果二叉搜索树能保持平衡,不退化成链表,它的查找、插入、删除的耗时是 ,即 ,乍一看哈希表都是完胜。
但哈希表就是代替不了二叉搜索树,理由主要有:
-
哈希表只能存无序的数据,二叉搜索树用 时间中序遍历就能获得有序数据
-
哈希表的扩容很耗时间,如果触发了哈希碰撞,哈希表的性能会不稳定。但二叉搜索树(平衡)能稳定在
-
二叉搜索树(平衡)的 与哈希表的 ,表面看着是哈希表更优秀,在实际用起来,两者差不了多少
-
哈希表要考虑很多东西,如设计好的哈希函数、冲突解决办法、扩容与缩容方案等。而二叉搜索树(平衡)只需要考虑如何保持它的平衡。