左神算法班 单调栈、Morris遍历(前、中、后序)及时间/空间复杂度分析

25 篇文章 0 订阅
19 篇文章 0 订阅

单调栈解决的问题:对于数组中的每一个数,可以找到这个数左面的最近的比他大的数和右面的最近的比他大的数。

例子:3 1 4 2 5,对于1来说,左面距离他最近的且比他大的数是3,右面距离他最近的且比他大的数是4;对于4来说,左面距离他最近的且比他大的数没有,右面距离他最近的且比他大的数是5.

如果用暴力法求,那么时间复杂度是O(n^2),因为你要遍历数组中的每一个数,对于每次遍历,都对这个数的左侧与右侧再做一次遍历找到距离他最近的且比他大的值。

而单调栈可以在O(n)内求解。

理解:
现有一数组:[3, 5, 4, 2],使用栈结构实现上述要求。

  • 开始遍历,i=0,将3入栈
  • i=1,5比栈顶元素3大,因此将3出栈,所以3是因为5出栈的,因此3的右侧距离他最近且比它大的数就是5,而此时栈中只有一个元素,所以3的左侧没有比他大的数,3出栈后栈为空,将5入栈
  • i=2,由于4比5小,因此将4入栈
  • i=3,2比4小,将2入栈
  • i=4,数组遍历完毕,此时栈顶元素为2,将其出栈,由于2不是因为碰见了某个比他大的数才出栈的,而是由于遍历到数组末尾才出栈的,所以2的右侧没有比他大的数,而2在栈中的下一个元素是4,所以4是2的左侧距离2最近且比他大的数
  • 4出栈,4之所以出栈不是因为遇见了比他大的数,而是由于遍历到了数组末尾,所以4的右侧也没有比他大的数,而4在栈中的下一个元素是5,所以5是4的左侧且第一个比4大的数
  • 5出栈,同理5不是因为碰见了比他大的数才出栈的,因此5的右侧没有比他大的数,而5又是栈中的最后一个元素,所以5的左侧也没有比他大的数。

特殊情况:有相等的元素如何处理?

数组[3, 3, 8],那么按照上面说的过程,先将0位置的3入栈,接着到1位置,但1位置的3和0位置的3相同,这时将1位置的3和0位置的3放在同一个位置,如下图所示:

在这里插入图片描述
当遍历到2位置时,由于8比栈顶元素3大,因此将3出栈,所以0和1位置的3都要出栈,且具体他们右侧的第一个比他们大的数为8。

下面来看看单调栈的应用

题目 1

在这里插入图片描述
我们可以先看一个预备问题:给你一个数组,把它想象成一个直方图,每个数字就代表直方图的高度,让你求这个数组中能构成的最大面积(必须是长方形),例子如下图所示,那么下图的最大面积就是10(黄色或绿色区域):

在这里插入图片描述

那么怎么解这个问题呢?前面提到了单调栈,是说可以找出一个数组中每个数的两侧第一个比他大的数,我们便可用这个思路来解题,只不过这次找的是距离某个数最近的比他小的数:

  • 开始遍历数组,刚开始i=0,由于栈中为空,因此将4入栈
  • i=1,此时arr[1] = 3,比栈顶元素4小,由于这次我们找的是两侧第一个比某个数小的数,因此4要出栈,这就意味着4代表的直方图的面积要开始计算了,由于4是目前栈中唯一元素,因此4的左侧没有元素,而右侧是3,所以4代表的直方图的面积是4*1=4,然后3进栈
  • i=2,此时arr[2] = 2,比栈顶元素3小,因此处理逻辑和上一步相同,只不过3目前是栈中唯一元素,因此左侧要从数组开始位置算起,右侧到2,所以3出栈时的面积是2*3=6,然后2入栈
  • i=3,此时arr[3] = 5,比栈顶元素2大,直接入栈
  • i=4,此时arr[4] = 6,比栈顶元素5大,直接入栈
  • i=5,超出数组范围,而栈中还有元素,因此开始出栈,栈顶元素是6,6不是因为碰见某个更小的数才出栈,而是数组遍历完了才出栈的,因此6的右边界即为数组最右侧,而6在栈中的下一个元素是5,代表其左边界是5,所以6出栈的产生面积是1*6=6
  • 此时5即将出栈,5也是因为数组遍历完了才出栈的,所以有边界是数组最右,5他下一个元素是2,所以其左边界是2,所以5出栈产生的面积是2*5=10
  • 2出栈,2同样是因为数组遍历完了才出栈的,所以有边界是数组最右,但2已经为栈中最后一个元素,因此2的左边界为数组最左,所以2出栈产生的面积是5*2=10
  • 因此上题的结果就是10

看明白了这个问题,就可以讨论最开始的问题了:对于给出的二维数组,我们对每一行都进行上面预备问题的算法,具体过程如下:

  • 开始遍历二维数组,首先是第一行 1 0 1 1,我们运行预备问题的算法即可:
    在这里插入图片描述

  • 然后是第二行,此时我们将第一行的数与第二行相加,但如果第二行某个位置为0,则不必与第一行相加,而是直接为0,例如原题的第二行和第一行进行相加后:2 1 2 2,此时再运行预备问题的算法即代表以第二行为底来算面积:
    在这里插入图片描述

  • 第三行,和第二行接着相加,注意第三行最后一个元素为0,所以结果为:3 2 3 0,然后运行预备问题算法:
    在这里插入图片描述

  • 所以最终结果应该是6。

也可以这么想:由于每次进栈的时候规则是:比自己大的直接进,所以某个数进栈了意味着他的下一个元素肯定比自己小,这就已经找到了左侧第一个比自己小的数,而这个数出栈就意味着肯定碰到了一个数比自己小,这就找到了右侧第一个比自己小的数,所以对这个数来说,他能产生的最大面积就是自己的值*左右两侧第一个比自己小数所形成的区域

题目 2

给你一个数组,例如[1, 2, 4, 5, 3],将每个数想象成一个山峰,数组中的数围成一圈,如下图所示:

在这里插入图片描述
有如下规定:

  • 两座相邻的山峰可以互相看见,如(1, 2)、(5, 3)等
  • 两座不相邻的山峰,若中间所有山峰的高度都小于等于这两座山峰中的较小值,那么这两座不相邻的山峰也能互相看见,如(3, 2),虽然3-5-4-2是一条路径,但沿途的数比3和2中的较小值大,因此这条路不能使3和2互相看见。但3-1-2是另一条路径且路径上的数都比2小,因此3和2仍然可以互相看见。

现在让你求给定数组中能互相看见的山峰有多少对。

分析:

  • 若数组中只有一个数,那肯定是0对
  • 若数组中有两个数,那就是1对
  • 若数组中有三个及以上互不相同的数,则一共有 2*元素个数 - 3对,证明如下:

我们把一个数组看成一个环,并让小的数去找大的数,例如有2 3,让2去找3,也就是说(2,3)是一对,那么(3,2)就不必再找了,见下图:

在这里插入图片描述
假设一个数i去向两侧找,只要他不是最大的数和第二大的数,那他就肯定能找到2个比自己大的数,即他会与这两个数分别构成一对可以相互看见的山峰且只能构成两对,因为他和其他的山峰会被附近这两个给隔开。设一共有n个数,那么除了最大的和第二大的还剩n-2个,而这n-2个数每一个都能找到2个比自己大的,因此一共是2*(n-2)个对,而最大的数和第二大的数之间还能构成一对,所以答案为2n-3对。

注意,上述证明是针对所有数互不相同的情况说的。

下面开始解题,先看遍历时的情况:

  • 先在所有数中找到一个最大的数,从他开始遍历,然后准备一个单调栈,从栈底到栈顶是从大到小,因此这个单调栈找的是每个数左右距离他最近且比他大的数。然后将这个最大的数放入栈底,1代表5出现了一次:
    在这里插入图片描述

  • 从5开始遍历,假设碰见了一个4,由于4小于5,满足我们需要的单调栈的规则,因此将4入栈:
    在这里插入图片描述

  • 继续,假设又碰到了一个3,由于3小于4,因此将3入栈:
    在这里插入图片描述

  • 此时遇见了一个4,由于4大于栈顶元素3,所以对于3来说,已经找到了两侧距离他最近且比他大的数,因此3出栈,并产生两对可互相看见的山峰:(3, 4)和(3, 4),之后将新碰见的4入栈:
    在这里插入图片描述

  • 此时又碰见了两个4,所以分别将他们入栈:
    在这里插入图片描述

  • 此时遇见了5,所以要将4个4全部出栈,而这4个4两两之间都能看见,因此会产生C(4,2)对山峰,而边缘的两个4又分别与1个5相连,所以每个4又能与每个5产生一对,那么4个4与2个5会产生42=10对,所以结果就是C(n,2)+n2
    在这里插入图片描述
    因此可以得出结论,对于下图所示的栈,会产生多少对山峰呢?
    在这里插入图片描述
    会产生C(k, 2)+k * 2 + C(m, 2)+m * 2 + C(n, 2)+n * 2对山峰。

以上是遍历时的情况,但在遍历完成后栈中可能还有若干元素,这时处理方法如下:

在这里插入图片描述
对于3来说,他在栈顶且没有因为碰见更大的数弹出,必然是因为数组遍历了一圈之后又回到了最大值的位置,所以这个3的一侧肯定是5,而4又是3的下一个元素,因此3的一侧也必然是4,所以对于3来说,它能产生的山峰对仍然是C(2, 2)+2 * 2

对于4来说,如果4在栈中的下个元素(在上图中即为5)出现了2次及以上,那么4出栈产生的山峰对仍然是那个公式:C(2, 2) + 2 * 2
如果4在栈中的下个元素(在上图中即为5)出现了1次,就说明这些4只和一个5相连。所以产生C(2, 2) + 2 * 1
在这里插入图片描述
对于最后一个元素(上图中的5)来说,如果只有一个,那么就是0对,如果有两个,就是1对,有三个及以上,就是C(n, 2)对。

代码:

class Pair{
    int value;
    int times;

    public Pair(int value) {
        this.value = value;
        this.times = 1;
    }
}

public class Mountain {
    public long solution(int[] arr) {
        if (arr == null || arr.length < 2) {
            return 0;
        }

        int maxIndex = 0;
        for (int i = 0; i < arr.length; i++) {
            maxIndex = arr[maxIndex] < arr[i]? i: maxIndex;
        }
        int next = getIndex(arr, maxIndex);
        long res = 0L;
        Stack<Pair> stack = new Stack<>();
        stack.push(new Pair(arr[maxIndex]));
        while (next != maxIndex) {
            int value = arr[next];
            while (!stack.empty() && stack.peek().value < value) {
                int times = stack.pop().times;
                res += combination(times) + times * 2;
            }
            if (!stack.empty() && stack.peek().value == value) {
                stack.peek().times++;
            }else {
                stack.push(new Pair(value));
            }
            next = getIndex(arr, next);
        }

        while (!stack.empty()) {
            System.out.println("peek: " + stack.peek().value + " times: " + stack.peek().times);
            int times = stack.pop().times;
            res += combination(times);
            System.out.println(res);
            if (!stack.empty()) {
                res += times;
                if (stack.size() > 1) {
                    res += times;
                }else {
                    res += stack.peek().times > 1? times: 0;
                }
            }
        }
        return res;
    }

    private long combination(int num) {
        return num == 1L? 0L: (long) num * (long) (num-1) / 2L;
    }



    private int getIndex(int[] arr, int size) {
        return size < (arr.length - 1)? (size+1): 0;
    }

    public static void main(String[] args) {
        int[] arr = new int[] {3, 3, 4, 4, 5, 5};
        System.out.println(new Mountain().solution(arr));
    }
}

结果:
11

题目 3 morris遍历

在这里插入图片描述
经典二叉树结构是没有指向父节点的指针的,因此在遍历时往往需要额外的空间复杂度O(h),h为二叉树的高度。这是因为需要栈空间来保存关于父节点的信息,如下图:

在这里插入图片描述
假设是先序遍历,即根-左-右。那么1输出之后要遍历2,2之后才是3,因此需要将1、2压栈,2遍历之后出栈,然后通过1再找到3,所以需要维持树高的额外空间。

morris遍历可以将空间复杂度降为O(1),主要思想是利用了二叉树的额外空间(即叶子节点的left和right,虽然他们指向null,但引用仍然占用空间)。

morris遍历的规则只有如下几点:

设cur初始指向头节点

  • 若cur无左孩子,则cur右移:cur = cur.right
  • 若cur有左孩子,找到cur左孩子的最右节点,记为mostRight
    • 若mostRight右指针指向null,则mostRight.right = cur,之后cur左移:cur = cur.left
    • 若mostRight右指针指向cur,则mostRight.right = null, 之后cur右移,cur = cur.right

为了理解上述过程,可看下图:
在这里插入图片描述
初始时cur指向头节点1,1有左孩子,所以找到1左孩子的最右节点5,记为mostRight,此时mostRight右指针为空,因此将其指向cur,然后cur左移到2,如下图所示:
在这里插入图片描述
cur目前指向2,2有左孩子,找4的最右节点,就是4自己,而4的右指针为mull,所以指向2,cur继续左移:
在这里插入图片描述
cur目前指向4,4无左孩子,所以cur右移到2,因为上一步已经将4的右指针指向2了:
在这里插入图片描述
cur目前指向2,有左孩子,但左孩子4的最右节点4指向cur,因此让4的右指针指向null,cur右移到5:
在这里插入图片描述
cur指向5,5无左孩子,因此cur直接右移到1,因为之前已经让5的右指针指向1了:
在这里插入图片描述
cur目前指向1,1有左孩子,但其最右节点5的右指针指向cur,因此让其指向空,cur右移到3:
在这里插入图片描述
此时cur指向3,有左孩子,其最右节点就是3的左孩子6本身,6的右指针为null,因此将其指向cur,之后cur左移到6:
在这里插入图片描述

cur指向6,6无左孩子,因此cur右移到3,3有左孩子,但其最右节点的右指针指向cur3,所以将6的右指针置为null,cur右移到7,7没有左孩子,cur右移为null,至此,整个遍历结束。

经典的二叉树遍历,对于每一个非空节点,都会被访问三次

public void traverse(root) {
	if (root == null) {return;}
	// 第一次访问
	traverse(root.left);
	// 第二次访问
	traverse(root.right);
	// 第三次访问
}

由于访问子节点后还需要退回到父节点才能访问其他的子节点,因此需要一个栈空间来存储沿途的父节点,这就是产生空间复杂度的原因。

而Morris遍历对于有左子树的节点会访问两次,对于没有左子树的节点只访问一次,访问一个子节点后并不需要额外的空间才能访问到其父节点的其他子节点,如下图所示,由于节点2的右指针指向了1,因此节点2遍历完之后会直接2.right到1,然后通过1访问3,并不会像经典方法那样借助栈才能访问到1Morris遍历充分利用了节点的空指针空间。
在这里插入图片描述
那么对于上图,Morris遍历的顺序就是1 2 1 3,如果我们想要先序遍历,那么就是123,也就是说在父节点第一次出现的时候打印即可;如果想要中序遍历,那就是213,这就需要在父节点第二次出现的时候打印

Morris代码:

public void preOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode cur = root;
        TreeNode mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    // 等于null说明cur被第一次访问
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    // 能走到这说明该节点已被第二次访问
                    mostRight.right = null;
                }
            }
            cur = cur.right;
        }
    }

时间复杂度分析

对于Morris遍历,每个节点至多被访问两次(有左子树的两次,没有的一次),而每次访问节点都要遍历到其左子树最右节点。我们不要纠结于每次访问到一个节点后又要经过多少次遍历才能找到其左子树最右节点,那样太难算了。

应该从宏观的角度来考虑,什么意思呢?对于每一个被访问的节点,它的左子树最右节点只有这个节点才会访问,见下图,当cur为1时,其左子树最右节点2 5会被遍历到,但只会被1遍历到且最多被遍历两次。对于2也一样,4就是他的左子树最右节点,所以4被访问两次。每一条右边界只会被某个特定的节点访问一到两趟,而一棵树可以被右边界分解,而每个节点最多被访问2次,共有n个节点,我们就算每个节点被访问两次,所以一共有2n个节点被访问,在这2n次访问中,所有的右边界都会被访问到2次,而所有的有边界又能拼成一棵树,那么就是2n次,所以一共有2n个节点被访问,在这个过程中共访问了2n个节点,所以时间复杂度仍然是O(n)。
在这里插入图片描述

前序遍历和中序遍历

由于存在左子树的节点会被访问两次,不存在左子树的节点只会被访问一次,因此选择不同的打印时机即可实现不同的遍历顺序,下面代码是先序和中序遍历:

class TreeNode {
    int value;
    TreeNode left;
    TreeNode right;

    public TreeNode(int value) {
        this.value = value;
    }
}

public class Morris {
    public void preOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode cur = root;
        TreeNode mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    // 等于null说明cur被第一次访问
                    System.out.print(cur.value + " ");
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    mostRight.right = null;
                }
            }else {
                System.out.print(cur.value + " ");
            }
            cur = cur.right;
        }
    }

    public void midOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode cur = root;
        TreeNode mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    // 等于null说明cur被第一次访问
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    System.out.print(cur.value + " ");
                    mostRight.right = null;
                }
            }else {
                System.out.print(cur.value + " ");
            }
            cur = cur.right;
        }
    }

    public static void main(String[] args) {
        TreeNode n1 = new TreeNode(1);
        TreeNode n2 = new TreeNode(2);
        TreeNode n3 = new TreeNode(3);

        n1.left = n2;
        n1.right = n3;

        new Morris().preOrder(n1);
        new Morris().midOrder(n1);

    }
}

上述的中序遍历midOrder可以进一步改写成如下形式:

public void midOrder(TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode cur = root;
        TreeNode mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    // 等于null说明cur被第一次访问
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    mostRight.right = null;
                }
            }
            // 只需在这里打印即可
            System.out.print(cur.value + " ");
            cur = cur.right;
        }
    }

因为一个节点如果没有左子树,那么它只会被访问一次;而有左子树的节点会被访问到两次,第一次会给左子树最右节点的右指针赋值为当前cur,此外cur左移,然后就continue了,也就是说第一次被访问时不会执行输出语句,只有第二次被访问到才会执行输出语句,因此只需一行。

后序遍历

这就有点麻烦了,还是用图说明吧:
在这里插入图片描述
上图后序遍历的顺序应该是4 5 2 6 7 3 1,观察后序遍历,可以发现其遍历顺序就是一条条的右子树,一棵二叉树可以被分解为一条条的右子树

在这里插入图片描述
因此,若是能像上图那样遍历二叉树就能得到后序遍历。
我们知道,Morris的遍历顺序为:1 2 4 2 5 1 3 6 3 7,在这里只关注那些可以被访问两次的节点,即2 1 3 ,并且在第二次访问时逆序输出它的左节点最右子树,以上图为例,2最先被第二次访问到,因此逆序输出它的左节点最右子树:4;接下来是1被第二次访问到,因此逆序输出它的左节点最右子树:5 2;然后是3,逆序输出: 6,此时所有被访问两次的节点都已找到,那么目前输出的结果是:4 5 2 6。之后再逆序输出头节点的最右的边:7 3 1,所以结果为:4 5 2 6 7 3 1

还有个问题:如何逆序输出?用栈行吗?行是行,但Morris遍历的特点就是空间复杂度O(1),所以要想其他方法:

链表的反转:假设要将1 3 7逆序输出,那么只需要将其看成一条链表,然后将链表反转即可,但别忘了将链表复原。

代码:

public void postOrder(TreeNode root) {
        if (root == null) {
            return;
        }

        TreeNode cur = root;
        TreeNode mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    mostRight.right = null;
                    // 逆序打印左节点的最右子树
                    printReverse(cur.left);
                }
            }
                cur = cur.right;
        }
        // 逆序打印头结点的最右边
        printReverse(root);
    }

    private void printReverse(TreeNode root) {
    	// 链表反转 
        TreeNode tail = reverseLink(root);
        TreeNode cur = tail;
        while (cur != null) {
            System.out.print(cur.value + " ");
            cur = cur.right;
        }
        // 恢复树结构
        reverseLink(tail);

    }
    private TreeNode reverseLink(TreeNode root) {
        TreeNode pre = null;
        TreeNode next = null;
        while (root != null) {
            next = root.right;
            root.right = pre;
            pre = root;
            root = next;
        }
        return pre;
    }

    public static void main(String[] args) {
        TreeNode n1 = new TreeNode(1);
        TreeNode n2 = new TreeNode(2);
        TreeNode n3 = new TreeNode(3);
        TreeNode n4 = new TreeNode(4);
        TreeNode n5 = new TreeNode(5);
        TreeNode n6 = new TreeNode(6);
        TreeNode n7 = new TreeNode(7);
        n1.left = n2;
        n1.right = n3;
        n2.left = n4;
        n2.right = n5;
        n3.left = n6;
        n3.right = n7;
        
        new Morris().postOrder(n1);
    }

结果:
4 5 2 6 7 3 1 

练习题 1 判断是否为搜索二叉树

通常的做法是中序遍历二叉树,如果值是递增的那就是搜索二叉树。而在Morris遍历中也可以做到这一点,只需在中序遍历需要输出的地方换成比较即可,如果前一个数大于等于当前的数,那么返回false:

public boolean isBST(TreeNode root) {
        if (root == null) {
            return true;
        }
        TreeNode cur = root;
        TreeNode mostRight = null;
        Integer pre = null;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    // 等于null说明cur被第一次访问
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    mostRight.right = null;
                }
            }
            if (pre != null && pre >= cur.value) {
                return false;
            }
            pre = cur.value;
            cur = cur.right;
        }
        return true;
    }

练习题 2 求一棵二叉树中叶子节点的最小高度

在这里插入图片描述
在这道题中,只有叶子节点才有高度,因此根节点的高度并不是1,除非只有根节点一个节点。

如果用经典递归求,那么就要求左子树的最小高度和右子树的最小高度,然后取较小的那个。

在这里插入图片描述
但能否用Morris来做?

首先要解决两个问题:

  • 能否获得当前节点的高度
  • 能否知道已经遍历到了叶子节点

首先看能否获得当前节点的高度,对于当前节点cur,有如下几种情况要考虑:

  • cur无左子树,意味着cur不存在被访问两次的情况,因此cur的高度+1
  • pre右移访问到当前节点,这时要分两种情况:
    • cur第一次被访问到,那么高度等于pre高度+1
    • cur第二次被访问到,高度等于pre高度减去它距离cur的节点数,如下图所示,当7访问1时,1的高度应该是7的高度4-之间的节点数(7 5 2) = 4 - 3 = 1
    • 那么对于cur,他怎么知道是不是第二次被访问到呢?看看pre.right = cur行吗?因为2访问5后,pre是2,cur是5,所以2.right=5,因此5的高度=2的高度+1。但这样做是不行的,因为当7访问1时,这时1第二次被访问,此时确实7.right = 1,那难道1的高度是7的高度+1吗?显然是不对的。正确的做法是看cur左孩子的最右节点是否指向自己,若是就应该减高度,用下图的例子就是看看1的左孩子(2)的最右节点(7)是否指向自己(是),所以1应该减高度。
      在这里插入图片描述

再看能否知道已经遍历到了叶子节点

在这里插入图片描述
设cur现在到了4,他其实是无法知道4是否为叶子节点的,因为4的右指针不为空。正确的做法应该是在第二次访问到2,并将4的右指针置空时再判断4是否左右指针都为空,若是则为叶子节点。

代码:

public int minHeight(TreeNode root) {
        if (root == null) {
            return 0;
        }
        TreeNode cur = root;
        TreeNode mostRight = null;
        int minHeight = Integer.MAX_VALUE;
        int curHeight = 0;
        int childNum ;
        while (cur != null) {
            mostRight = cur.left;
            childNum = 1;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                    childNum++;
                }
                if (mostRight.right == null) {
                    curHeight++;
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    mostRight.right = null;
                    if (mostRight.left == null) {
                        // 此if成立说明为叶子节点
                        minHeight = Math.min(minHeight, curHeight);
                    }
                    curHeight -= childNum;
                }
            }else {
                curHeight++;
            }
            cur = cur.right;
        }

        cur = root;
        int rightNum = 1;
        while (cur.right != null) {
            rightNum++;
            cur = cur.right;
        }
        if (cur.left == null) {
            minHeight = Math.min(minHeight, rightNum);
        }
        return minHeight;
    }

我们来使用下图过一遍上述代码:

在这里插入图片描述
curHeight表示cur的高度初始值为0,用minHeight来记录叶子节点的最小高度。

开始时cur指向1,mostRight可以跑到5,5的right是null,说明1是第一次访问,所以让5的right指向1,并将curHeight++,注意,此时的curHeight是1的高度。cur左移到2,mostRight会跑到4,4的right是null,说明2第一次访问,因此让其指向2,此时curHeight++(即2的高度)。cur左移到4,而4没有左孩子(想象一下,一个节点通过左移来到,那么它的高度必然是上个节点的高度+1),curHeight++,此时为3,意味着4的高度为3。之后cur右移到2,但发现mostRight的right指向自己,这意味着2是第二次访问到,因此用4的高度3减去2的左子树最右节点之间的高度1,即3-1=2为cur目前的高度,然后cur右移到5,5没有左孩子,所以curHeight++后cur右移到1,1的mostRight的right指向了1意味着1第二次被访问到,所以cur目前的高度是5的高度3减去1的左子树到最右节点的高度(2、5)2,即cur的高度为1。之后cur右移到3,而3无左孩子,继续右移,为null,退出循环。

注意,此时只是求出1 2 4 5的高度,1 3这个右边界还没有被访问过,因此还需走一遍1 3,当到达最右节点3时,还要判断一下3的左子树是否为空,只有为空才说明3是叶子节点,否则3不是叶子节点。在本例中3时是叶子结点,且高度为2。

因此上题的最终结果为2。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值