在计算机科学中,数据的相对大小比绝对的数值重要,出于很多数据比大小的需求以及其他一些需求,就产生了一个抽象的数据结构——二叉树
二叉树可以做很多事情,比如排序、快速查找到某一个数值、根据网站目录结构下载所有网页、组织里的管理结构。所有能够对应到二叉树的问题,都有一些共性,解决它们之间问题的方法是可以触类旁通的。
排序
二叉树可以直接完成排序,因为它有一个很好的性质,就是它左右两个分叉可以和比较大小后的两种结果自然对应起来。
二叉树排序的过程遵循下面两条规则:
1.先来的占据根部,以及靠近顶部层级比较高的位置,后来的放在相对靠下的位置。
2.每当一个分支的根部被占据之后,接下来的数字,是和根部的数字进行比较,小的放到左边分叉中,大的放到右边分叉中。
这样安排得出来的二叉树,里面的数字从左到右自然是从小到大排好序的。
比如我们对(3、5、-2、0、6)这组数排序。
1.把第一个数3放到二叉树的顶端(根部)。
2.把第二个数5拿来,和树根部的那个数3做对比,由于5大于3,就放在右边的叉树中,由于它是右边叉树上的第一个数字,因此就占据了右边叉树根部的位置。
3.把第三个数-2拿来,和根部的那个数3对比一下,这次-2比3小了,于是放在左边。同样,因为它是左边第一个数,因此占了左边叉树的根的位置。
4.把第四个数0拿来,和根部的3对比一下,因为它比3小,所以要放在左边的叉树中。但是,左边叉枝的根部已经被-2占据了,因此0需要放到以-2为根部的左边的叉树下一层的某个位置,至于放在这个叉树的左边还是右边,需要再和-2比较一次,来决定它的位置。由于0比-2大,因此放到了-2的右边。
5.把第五个,也就是最后一个数6拿来,先和根部的3比较一下,发现它比3大,因此放在右边的叉树中,但是右边的叉树根部已经被5占据,没有地方了,于是和右叉树的根结点5比较了一下,发现比5大,因此要放在右叉树的右边。
把二叉树中的数字从左到右取出来即可,最左边的是-2,然后是0,中间根部是3,往右边是5和6。
这样一来,把一堆数字按照一定的规则放到二叉树中,再拿出来,它们就有序了
完全二叉树:除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
链式存储法
每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。
顺序存储法
如果节点 X 存储在数组中下标为 i 的位置,下标为 2 * i 的位置存储的就是左子节点,下标为 2 * i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。
如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这也是为什么完全二叉树会单独拎出来的原因,也是为什么完全二叉树要求最后一层的子节点都靠左的原因。
前序遍历,对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
中序遍历,对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
后序遍历,对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。
遍历的过程是一个递归的过程。遍历操作的时间复杂度,跟节点的个数 n 成正比,也就是说二叉树遍历的时间复杂度是 O(n)
void preOrder(Node* root) {
if (root == null) return;
print root // 此处为伪代码,表示打印root节点
preOrder(root->left);
preOrder(root->right);
}void inOrder(Node* root) {
if (root == null) return;
inOrder(root->left);
print root // 此处为伪代码,表示打印root节点
inOrder(root->right);
}void postOrder(Node* root) {
if (root == null) return;
postOrder(root->left);
postOrder(root->right);
print root // 此处为伪代码,表示打印root节点
}
二叉树查找:先取根节点,如果它等于要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。
二叉查找树的插入操作:如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置。
二叉查找树的删除操作:
1.如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。
2.如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点。
3.如果要删除的节点有两个节点,需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了)。
支持重复数据的二叉树:
方式1.二叉查找树中每一个节点不仅会存储一个数据,因此我们通过链表和支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节点上。
方式2.把相同的元素统一放在右子树或左子树。
完全二叉树的高度小于等于 log2n。
【数据结构与算法|重温算法Day13:二叉树】和散列表对比优势
1.散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。二叉查找树只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。
2.散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定,平衡二叉树性能稳定,O(logn)。
3.尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小。
4.散列表的构造比二叉查找树要复杂,如:散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只考虑平衡。
推荐阅读
- 剑指Offer|牛客网——python之剑指0ffer之67道在线编程——jz16-jz20
- 数据结构|数据结构(树(Tree)【详解】)
- 数据结构|树的概念及其应用(Python)
- 存储|哈夫曼树(Huffman tree)及哈夫曼编码
- 数据结构算法|二叉树链式存储之 前序,中序 ,后序遍历 查找
- 从零开始学习数据结构|学习数据结构--第四章(树与二叉树(二叉树的顺序存储和链式存储))
- 数据结构与算法|详解线索二叉树
- 代码分享|C语言手写二叉树(链式存储结构)
- 算法计算经典|二叉树知识点最详细最全讲解