数据结构与算法基础六:二叉树

一:定义

首先回忆一下二分法,猜一个100以内的整数,先从50开始,然后获得结果,结果有三种,大,小,相等;我们忽略结果,考虑过程,那就只有大和小,类似的还有true or false,等等两种走向的分支都可以用二叉树来描述.
1.二叉树的每个节点最多有2个子节点.
2.二叉树是有序树,左右子节点有顺序,左右子树自然也是有顺序的.
3.即使只有一个子节点,也是区分左右的.

二叉树的五种基本形态:
1.空树;
2.只有一个根节点;
3.根节点和左子树;
4.根节点和右子树;
5.根节点和左子树和右子树.

特殊的二叉树:
1.斜树,所有节点只有左子树的叫做左斜树,相反叫右斜树;
2.满二叉树:所有的节点都有左右子树,所有的叶节点都在同一层,也就是说每个节点左右子树节点都相等,是对称的.
3.完全二叉树:二叉树按层编号,从上到下从左到右,如果满足满二叉树的子集,那么称为完全二叉树


数据结构与算法基础六:二叉树_第1张图片
从上到下从左到右编号

数据结构与算法基础六:二叉树_第2张图片
这些都不是完全二叉树

总结一些完全二叉树的特点:
叶节点一定在最下面两层
最下层的叶节点一定在左边
倒数第二层的叶节点一定在右边
如果度是1,那就只有左子树
完全二叉树不一定是满二叉树,满二叉树一定是完全二叉树,包含关系

二:二叉树的性质

数据结构与算法基础六:二叉树_第3张图片
对照图

首先满二叉树每一层节点数是等比数列,因此:
1.第i层的节点数<=2ⁱ⁻¹个;
2.深度为k的二叉树节点数<=2ᴷ - 1个;

其次:
3.对与任何一个二叉树T,总节点数n,其中叶节点个数为n₀,度为1的节点数为n₁,度为2的节点数为n₂;
因此n=n₀+n₁+n₂;
分支总数s = n-1 = n₀+n₁+n₂-1 = n₁+2*n₂,因此得到n₀=n₂+1.

另外对于完全二叉树:
一个节点总数为n的完全二叉树,深度为(<>在这里表示取整);对节点按层编号i(1<=i<=n),
如果i=1,则是根节点;如果i>1,那么i的父节点是;
如果2i>n,则i没有孩子;否则i的左孩子是2i;
如果2i+1>n,则i没有右孩子,否则i的右孩子是2i+1;

三:二叉树存储结构

1.顺序存储
对于完全二叉树,我们把节点存储在数组中

数据结构与算法基础六:二叉树_第4张图片
完全二叉树

可以看到由于完全二叉树的性质,即使顺序存储也能体现出来.
数据结构与算法基础六:二叉树_第5张图片
把节点存储在数组中

但是如果不是完全二叉树,可以选择把没有孩子的地方空出来,虽然会有空间的浪费,如果是一个所有节点都只有右孩子的二叉树,深度是k的同时节点数也是k,缺需要分配2ᴷ-1个空间.
数据结构与算法基础六:二叉树_第6张图片
image.png

2.二叉链表
二叉树固定0~2个孩子,整个树都是可以分解成这个结构,因此定义一个这样的结构来表示节点,数据域,左孩子的指针域和右孩子指针域.

二叉链表的节点

数据结构与算法基础六:二叉树_第7张图片
二叉链表

四:遍历二叉树

既然是遍历,那就得雨露均沾每个节点都要访问,而且每个节点还只能访问一次.
1.前序遍历
父节点 -> 左孩子 -> 右孩子
从根节点开始,先到达根节点的左孩子,然后左孩子的左孩子,一直到叶节点,接着是叶节点的兄弟,然后向上回溯,找到右孩子,重复这个过程.

数据结构与算法基础六:二叉树_第8张图片
前序遍历

2.中序遍历
左孩子 -> 父节点 -> 右孩子
从最左边的叶节点开始,然后父节点,然后右孩子,然后到父节点的右兄弟,一路回到根节点,接着是右子树,重复这个过程.

数据结构与算法基础六:二叉树_第9张图片
中序遍历

3.后序遍历
左孩子 -> 右孩子 -> 父节点
后序遍历关键在于兄弟节点,首先从最左边的叶节点开始,然后兄弟节点,然后回到父节点,然后是父节点的右兄弟,重复之前的过程.

数据结构与算法基础六:二叉树_第10张图片
后序遍历

4.层序遍历
简单直接,从上到下从左到右

数据结构与算法基础六:二叉树_第11张图片
层序遍历

五:算法实现

1.使用递归实现前序遍历
当节点访问左孩子的时候,左孩子再去访问左孩子,一直到子节点,函数才会返回,之后再访问右孩子,右孩子又是继续访问左孩子,一直到叶节点.

数据结构与算法基础六:二叉树_第12张图片
伪代码

swift实现

//首先定义节点,节点有数据域,和两个孩子指针
class Node{
    var name : String
    var leftChild : Node?
    var rightChild : Node?
    init(_ dataName:String) {
        name = dataName
    }
}

//定义遍历的方法,如果没有孩子,则函数返回,如果有,就输出,然后继续访问孩子
func binaryTreeForEach(_ root : Node?){
        guard let node = root else {
            return
        }
        print(node.name)
        binaryTreeForEach(node.leftChild)
        binaryTreeForEach(node.rightChild)
    }

//然后组织一个二叉树,简化一下直接手写一个
        let n1 = Node.init("A")
        let n2 = Node.init("B")
        let n3 = Node.init("C")
        let n4 = Node.init("D")
        let n5 = Node.init("E")
        let n6 = Node.init("F")
        let n7 = Node.init("G")
        let n8 = Node.init("H")
        let n9 = Node.init("I")
        let n10 = Node.init("J")
        
        n1.leftChild = n2
        n1.rightChild = n3
        n2.leftChild = n4
        n4.rightChild = n5
        n3.leftChild = n6
        n3.rightChild = n7
        n6.rightChild = n8
        n7.leftChild = n9
        n7.rightChild = n10

//调用函数
 binaryTreeForEach(n1)

这个二叉树结构是这样的
_______A
____B_____C
_D______F___G
__E____H___I__J


数据结构与算法基础六:二叉树_第13张图片
输出顺序

总结前序遍历是父(左)(右),其中左可以递归(父(左)(右)),得到父(父(左)(右))(右),最左边一定是根节点,最右边是右孩子

2.使用递归实现中序遍历
通过递归可以发现,第一个被调用的,不一定是第一个被处理的,因此运用递归的特性,虽然中序遍历从叶节点开始,但是函数入口依然是根节点.
当节点访问完了左孩子时,输出并返回

数据结构与算法基础六:二叉树_第14张图片
伪代码

swift实现(还是上面的那个二叉树

func binaryTreeForEach(_ root : Node?){
        guard let node = root else {
            return
        }
        binaryTreeForEach(node.leftChild)
        print(node.name)
        binaryTreeForEach(node.rightChild)
    }

可见就改了一行代码,调换了输出位置.


数据结构与算法基础六:二叉树_第15张图片
输出顺序

总结中序遍历是(左)父(右),递归之后是((左)父(右))父(右),最左边是左孩子或者父节点,最右边是右孩子或父节点

3.用递归实现后续遍历
到这里就可以明白了,肯定是把输出放到最后去,当节点访问完了左右孩子时,函数输出并返回

数据结构与算法基础六:二叉树_第16张图片
伪代码

swift实现

func binaryTreeForEach(_ root : Node?){
        guard let node = root else {
            return
        }
        binaryTreeForEach(node.leftChild)
        binaryTreeForEach(node.rightChild)
        print(node.name)
    }
数据结构与算法基础六:二叉树_第17张图片
输出顺序

总结中序遍历是(左)(右)父,递归是((左)(右)父)(右)父,最右边一定是根节点.

  • 根据遍历推导二叉树的结构,例如一个二叉树前序遍历是ABCDEF,中序遍历是CBAEDF,那么它的后序遍历是什么:
    首先根节点是A;
    BC可能性很多但是B一定在左或者在C上面,CB可以是C左B父,也可以C父B右,BC和CB结合起来答案是B是父节点,C是左孩子;
    CBA可见回到了根节点A,那么EDF自然是右子树,同理DEF可以看出D是父节点,那么根据EDF得知E是左孩子F是右孩子.
    二叉树结构如下
    ______A
    ___B_____D
    C_____E____F
    那么后序遍历顺序是CBEFDA.

根据中序遍历,我们能分出那些可能是左子树,那些可能是右子树,再根据前序后续就可以推断出二叉树,因此:
前序+中序或者后序+中序可以推断二叉树,而前序+后序不行

六:生成二叉树

遍历二叉树得到了一个序列,那么反过来遍历这个序列就可以生成二叉树.
因此,给定一个序列和遍历方法,可以生成二叉树,但是只是这样还不行,因为经常不能确定是左孩子还是右孩子,因此要把每个节点的左右孩子都占位补满,叫做扩展,比如用特殊符号占位"AB#D##C##"


数据结构与算法基础六:二叉树_第18张图片
二叉树AB#D##C##

数据结构与算法基础六:二叉树_第19张图片
补上每个节点的左右孩子

swift也是利用递归来实现

//调用
   let node = createBinaryTree(["A","B","#","D","#","#","C","#","#"])
//正序遍历输出
   binaryTreeForEach(node)

//实现
func createBinaryTree(_ list:[String]) -> Node?{
        if(list.count == 0){
            return nil
        }
        var index = 0
        
        func createTree() -> Node?{
            let str = list[index]
            var tree : Node?
            index += 1;
            if index < list.count && str != "#"{
                tree = Node.init(str)
                tree!.leftChild = createTree()
                tree!.rightChild = createTree()
            }
            return tree
        }
        
        return createTree()
    }

数据结构与算法基础六:二叉树_第20张图片
输出结果

扩展后的先序遍历可以确定一个二叉树,但是中序遍历不行,中序遍历一定会出现#..#..#..#..#..#的情况.

你可能感兴趣的:(数据结构与算法基础六:二叉树)