算法進階面試題03——構造數組的MaxTree、最大子矩陣的大小、2017京東環形烽火臺問題、介紹Morris遍歷並實現前序/中序/後序

接着第二課的內容和帶點第三課的內容。node

 

(回顧)準備一個棧,從大到小排列,具體參考上一課....面試

 

 

 

構造數組的MaxTree

【題目】數組

定義二叉樹以下:app

public class Node{函數

    public int value;this

    public Node left;spa

    public Node right;3d

 

    public Node(int data){指針

        this.value=data;code

    }

}

一個數組的MaxTree定義以下:

◆ 數組必須沒有重複元素

◆ MaxTree是一顆二叉樹,數組的每個值對應一個二叉樹節點

◆ 包括MaxTree樹在內且在其中的每一顆子樹上,值最大的節點都是樹的頭。

給定一個沒有重複元素arr,寫出生成這個數組的MaxTree函數,要求若是數組長度爲N,則時間複雜度爲O(N),額外空間複雜度爲O(N)。

 

 

解法一:建出大根堆再重連成徹底二叉樹,o(n)

解法二:單調棧

一、左右都沒比他大的節點就是頭節點

二、對於有左右其中一邊的,這個數就串在有的底下,3在4底下

三、對於左右都有的,掛在左右中較小的數下,2在3底下

 

 

爲何可行?

爲何不會造成森林?

數組中沒有重複值,最大值確定是頭節點,任何節點都串在比他大的,都有歸屬,最終以最大值爲頭部,因此是一棵樹。

會不會出現一個節點多個孩子的狀況?

首先先證實在任意一側只有一棵樹掛在他底下。

 

 

反證法:假設b、c都掛在a底下。

b>c的話,不可能c還會掛在a底下,存在矛盾。

c>b的話,b也不可能掛在a下,會掛在c底下。

 

老師版本(分開設置左右臨近的最大值)

//構造數組的MaxTree
public class Code_02_MaxTree {

    //二叉樹結點的定義以下
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        //結點的初始化
        public Node(int data) {
            this.value = data;
        }
    }

    //構造數組的MaxTree函數
    public static Node getMaxTree(int[] arr) {
        Node[] nArr = new Node[arr.length];  //存放二叉樹結點的數組
        for (int i = 0; i != arr.length; i++) {
            nArr[i] = new Node(arr[i]);
        }
        Stack<Node> stack = new Stack<Node>();//利用棧找出左右邊第一個比自身大的數
        HashMap<Node, Node> lBigMap = new HashMap<Node, Node>();
        HashMap<Node, Node> rBigMap = new HashMap<Node, Node>();
        //從第一個開始
        // 找出全部數,左邊第一個比自身大的數
        for (int i = 0; i != nArr.length; i++) {
            Node curNode = nArr[i];
            while ((!stack.isEmpty()) && stack.peek().value < curNode.value) {
                popStackSetMap(stack, lBigMap);//棧的一系列操做
            }
            stack.push(curNode);
        }
        while (!stack.isEmpty()) {
            popStackSetMap(stack, lBigMap);
        }
        //從最後一個開始
        // 找出全部數,右邊第一個比自身大的數
        for (int i = nArr.length - 1; i != -1; i--) {
            Node curNode = nArr[i];
            while ((!stack.isEmpty()) && stack.peek().value < curNode.value) {
                popStackSetMap(stack, rBigMap);//棧的一系列操做
            }
            stack.push(curNode);
        }
        while (!stack.isEmpty()) {
            popStackSetMap(stack, rBigMap);
        }
        Node head = null; //聲明頭結點
        for (int i = 0; i != nArr.length; i++) {
            Node curNode = nArr[i];
            Node left = lBigMap.get(curNode);
            Node right = rBigMap.get(curNode);
            //左右都空,證實他是最大的頭節點
            if (left == null && right == null) {
                head = curNode;
            } else if (left == null) {//左爲空,就串在右的底下
                if (right.left == null) {//從左到右
                    right.left = curNode;
                } else {
                    right.right = curNode;
                }
            } else if (right == null) {//右爲空,就串在左的底下
                if (left.left == null) {//從左到右
                    left.left = curNode;
                } else {
                    left.right = curNode;
                }
            } else {
                //選擇左右較小的數爲父節點
                Node parent = left.value < right.value ? left : right;
                if (parent.left == null) {//從左到右
                    parent.left = curNode;
                } else {
                    parent.right = curNode;
                }
            }
        }
        return head;
    }

    //棧的一系列操做
    public static void popStackSetMap(Stack<Node> stack, HashMap<Node, Node> map) {
        Node popNode = stack.pop();

        if (stack.isEmpty()) {
            map.put(popNode, null);
        } else {
            map.put(popNode, stack.peek()); //構造二叉樹操做
        }

    }

    //二叉樹的先序遍歷
    public static void printPreOrder(Node head) {
        if (head == null) { return; }
        System.out.print(head.value + " ");
        printPreOrder(head.left);  //遞歸調用遍歷二叉樹
        printPreOrder(head.right);
    }

    //二叉樹的中序遍歷
    public static void printInOrder(Node head) {
        if (head == null) { return; }
        printInOrder(head.left);
        System.out.print(head.value + " ");
        printInOrder(head.right);
    }


    public static void main(String[] args) {
        int[] arr = {3, 4, 5, 1, 2};
        Node head = getMaxTree(arr);
        printPreOrder(head);
        System.out.println();
        printInOrder(head);
    }
}

 

本身碼的版本(彈出一個數,同時設置其左右臨近的最大值)

 

public class a03_01MaxTree {
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }

    public static Node getMaxTree(int[] nodes) {
        Stack<Integer> stack = new Stack<Integer>();
        HashMap<Integer, Integer> lLarge = new HashMap<Integer, Integer>();
        HashMap<Integer, Integer> rLarge = new HashMap<Integer, Integer>();

        for (int i = 0; i != nodes.length; i++) {
            while (!stack.isEmpty() && nodes[i] > nodes[stack.peek()]) {
                int index = stack.pop();
                //誰讓它彈出誰就是右邊比他大的
                rLarge.put(index, i);
                //再經過棧來設置左邊的狀況
                if (stack.isEmpty()) {
                    lLarge.put(index, null);
                } else {
                    lLarge.put(index, stack.peek());
                }
            }
            stack.push(i);
        }
        //把棧裏面剩下的元素處理下
        while (!stack.isEmpty()) {
            int index = stack.pop();
            rLarge.put(index, null);
            if (stack.isEmpty()) {
                lLarge.put(index, null);
            } else {
                lLarge.put(index, stack.peek());
            }
        }

        //造成Node數組
        Node[] treeNodes = new Node[nodes.length];
        for (int i = 0; i != nodes.length; i++) {
            treeNodes[i] = new Node(nodes[i]);
        }
        //建樹
        Node head = null;
        for (int i = 0; i != nodes.length; i++) {
            Integer lLargeIndex = lLarge.get(i);
            Integer rLargeIndex = rLarge.get(i);
            if (lLargeIndex == null && rLargeIndex == null) {
                head = treeNodes[i];
            } else if (lLargeIndex == null) {
                if (treeNodes[rLargeIndex].left == null) {
                    treeNodes[rLargeIndex].left = treeNodes[i];
                } else {
                    treeNodes[rLargeIndex].right = treeNodes[i];
                }
            } else if (rLargeIndex == null) {
                if (treeNodes[lLargeIndex].left == null) {
                    treeNodes[lLargeIndex].left = treeNodes[i];
                } else {
                    treeNodes[lLargeIndex].right = treeNodes[i];
                }
            } else {//對比左右大小
                Node parent = nodes[lLargeIndex] > nodes[rLargeIndex] ? treeNodes[rLargeIndex] : treeNodes[lLargeIndex];
                if (parent.left == null) {
                    parent.left = treeNodes[i];
                } else {
                    parent.right = treeNodes[i];
                }
            }
        }
        return head;
    }

    //二叉樹的先序遍歷
    public static void printPreOrder(Node head) {
        if (head == null) { return; }
        System.out.print(head.value + " ");
        printPreOrder(head.left);  //遞歸調用遍歷二叉樹
        printPreOrder(head.right);
    }

    //二叉樹的中序遍歷
    public static void printInOrder(Node head) {
        if (head == null) { return; }
        printInOrder(head.left);
        System.out.print(head.value + " ");
        printInOrder(head.right);
    }


    public static void main(String[] args) {
        int[] arr = {3, 4, 5, 1, 2};
        Node head = getMaxTree(arr);
        printPreOrder(head);
        System.out.println();
        printInOrder(head);
    }

}

 

 

最大子矩陣的大小

  給定一個整型矩陣map,其中的值只有0,1兩種,求其中全是1 的全部矩陣區域中,最大的矩形區域爲1的數量。

例如:

  1  1  1  0

其中最大的矩形區域有3個1,因此返回3

例如:

  1  0  1  1

      1  1  1  1

  1  1  1  0

其中,最大的矩形區域有6個1,因此返回6

 

跳出這道題目,爲了解決這個問題的一個引子題。

直方圖找最大矩形

 

 

每一個數都表明一個矩形,從第一個數開始嘗試左右移動,碰到小的就停,例如4,面積就是四、3面積就是6....依次確定會計算出最大的矩形。

利用單調棧,找到最近比他小的元素,彈出的時候,就能夠計算出面積。

 

 

若是不是由於碰到比本身小的,所有元素事後,棧剩下的,就證實他們都能到達右邊界(擴到5停)。

相等的狀況:

 

 

原始問題,徹底借用了直方圖的結論。

先看必須由第0行做爲底,全部矩形中,哪一個含有1是最多的。

接着看由第1行爲底,每一個位置,上面有多少個連續的1,而後經過這個計算結果,計算出哪一個位置含1最多。

本次爲0必爲0,不然是以前的數再+1

 

 

依次計算....

矩陣遍歷一遍,o(n*m)搞定

代碼解說:

public class Code_04_MaximalRectangle {

    //height數組表示在以當前行做爲底的狀況下,每一個位置往上的連續的 1 的數量
    public static int maxRecSize(int[][] map) {
        if (map == null || map.length == 0 || map[0].length == 0) {
            return 0;
        }
        int maxArea = 0;
        //輔助數組
        int[] height = new int[map[0].length];
        for (int i = 0; i < map.length; i++) {
            for (int j = 0; j < map[0].length; j++) {
                height[j] = map[i][j] == 0 ? 0 : height[j] + 1;
            }
            maxArea = Math.max(maxRecFromBottom(height), maxArea);
        }
        return maxArea;
    }

    //找到柱子左邊剛比它小的柱子位置,以及右邊剛比它大的柱子位置,用棧計算最快
    //最基本的方法,若是一個數組表示直方圖的話,在其中找到最大正方形
    //例如:[4,3,2,5,6]表明一個直方圖
    private static int maxRecFromBottom(int[] height) {
        if (height == null || height.length == 0) {
            return 0;
        }
        int maxArea = 0;
        Stack<Integer> stack = new Stack<Integer>();
        //遍歷數組中的每個數
        for (int i = 0; i < height.length; i++) {
            //不斷循環,直到當前位置的值小於棧頂元素
            while (!stack.isEmpty() && height[i] <= height[stack.peek()]) {
                int j = stack.pop();//
                int k = stack.isEmpty() ? -1 : stack.peek();//左邊界
                int curArea = (i - k - 1) * height[j];
                maxArea = Math.max(curArea, maxArea);
            }
            stack.push(i);
        }

        // 針對的是從棧底到棧頂一直遞增的狀況
        //結算棧中剩下的東西
        while (!stack.isEmpty()) {
            int j = stack.pop();//
            int k = stack.isEmpty() ? -1 : stack.peek();//左邊界
            int curArea = (height.length - k - 1) * height[j];
            maxArea = Math.max(curArea, maxArea);
        }
        return maxArea;
    }

    public static void main(String[] args) {
        int[][] num = new int[][]{
                {0,1,0,0,0,0},
                {1,1,1,1,1,1},
                {1,1,1,1,1,1},
                {1,1,1,1,1,1},
                {1,1,1,1,1,1}
        };
        System.out.println(maxRecSize(num));
    }

}

 

 

2017年京東原題:烽火臺

給一個數組,表明環形的山

一、每座山上會放烽火,相鄰可看見烽火

二、不相鄰的山峯,兩條路中其中一條路上的烽火都不大於他們之間的最小值,就能看見

返回能相互看見的山峯有多少對?

 

 

簡單問法:數組值都不同,o(1)。

 

2*i-3對

證實:

把找法規定爲:永遠是小去找大。

3個數以及上,找到最高和次高,假設中間有一個值i,從i出發找大的,左邊找到比i大的X停,右邊找到Y停,就是兩對。(最高和次高之間的每一個數都能找到兩個山峯對)

 

 

因此就是((n-(最高+次高))*2)+1(最高和次高那條)

((n-2)*2)+1 = 2n-4+1 = 2n-3條

 

有重複值的狀況:單調棧

找到第一個最大值,開始日後遍歷

加入碰到4,入棧,碰到5,開始出棧,由於底部是5,1(表明5出現了1次),因此對於4來講找到兩對能夠看見的山峯。接着底部5+1,變成5,2。

 

 

大致思路仍是小的找大的。(換成4也同理)

 

 

若是連續都是4,直到4,4碰上5,會產生兩種互相看見的狀況。

一、4和4以前相互看見,任意兩個4都能相互看見,C42+4*2。

二、每一個4左右邊都能看見5,4*2。

 

 

用最大值打底的緣由:在結算的過程當中由於有這個最大值的出現,能夠肯定這個數的順時針方向能夠找到比他大的。

 

最後棧裏面還有東西,須要結算。

 

 

三條及以上的仍是用那個通式

 

若是底下是2以上,依舊是通式

 

 

若是底下只有1個5(兩邊遇到最大的是同一個5),計算變成:

 

 

 

因此,倒數第二的數要考慮,最大值的數量

最後一個數,比一大就是單純的Ck2,若是隻有一個就是0對。

 

public class Code_05_MountainsAndFlame {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNextInt()) {
            int size = in.nextInt();
            int[] arr = new int[size];
            for (int i = 0; i < size; i++) {
                arr[i] = in.nextInt();
            }
            System.out.println(communications(arr));
        }
        in.close();
    }

    public static class Pair {
        public int value;
        public int times;

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

    private static long communications(int[] arr) {
        if (arr == null || arr.length < 2) {
            return 0;
        }
        int size = arr.length;
        int maxIndex = 0;
        for (int i = 0; i < size; i++) {//得到最大值的下標
            maxIndex = arr[maxIndex] < arr[i] ? i : maxIndex;
        }
        int value = arr[maxIndex];//最大值
        int index = nextIndex(size, maxIndex);//最大值位置的下一個
        long res = 0L;//能互相看見的山峯數
        Stack<Pair> stack = new Stack<Pair>();
        stack.push(new Pair(value));//以最大值爲底
        while (index != maxIndex) {//轉一圈
            value = arr[index];
            while (!stack.isEmpty() && stack.peek().value < value) {
                int times = stack.pop().times;
//                res += getInternalSum(times) + times;
//                res += stack.isEmpty() ? 0 : times;
                res += getInternalSum(times) + 2 * times;
            }
            if (!stack.isEmpty() && stack.peek().value == value) {
                stack.peek().times++;//相等的話累加
            } else {
                stack.push(new Pair(value));
            }
            index = nextIndex(size, index);
        }
        //跑完一圈後,結算棧裏面剩下的
        while (!stack.isEmpty()) {
            int times = stack.pop().times;
            //無論什麼,確定會內部產生山峯對
            res += getInternalSum(times);
            if (!stack.isEmpty()) {//沒到最後一個
                res += times;
                if (stack.size() > 1) {//3個及以上的狀況
                    res += times;
                } else {//第2個的狀況,要結合最後一個數來判斷
                    res += stack.peek().times > 1 ? times : 0;
                }
            }
        }
        return res;
    }

    //簡單Ck(下標)2(上標)
    private static long getInternalSum(int n) {
        return n == 1L ? 0L : (long) n * (long) (n - 1) / 2L;
    }

    private static int nextIndex(int size, int i) {
        return i < (size - 1) ? (i + 1) : 0;
    }
}

 

作這些就是爲了進本身想進的任何公司,加油!

筆試分數會決定是否籤合同,按部門挑人。

 

開始第三課的內容

 

一、介紹一種時間複雜度O(N),額外空間複雜度O(1)的二叉樹的遍歷方式,N爲二叉樹的節點個數

經典二叉樹,不管遞歸仍是非遞歸都逃不過,o(h)的額外空間複雜度,h爲高度。

 

二、Morris遍歷

利用Morris遍歷實現二叉樹的先序,中序,後序遍歷,時間複雜度O(N),額外空間複雜度O(1)。

利用了底層空閒的空間,在學術上稱爲線索二叉樹

 

Morris遍歷過程當中的標準

 

②...指向cur,讓其指向空,cur向右移動

 

首先先忘掉什麼前序中序後序遍歷,這就叫Morris序

按標準走徹底部流程。

 

看一下本質:

只要這個節點有左子樹,就能回到這個節點兩次,若是沒有左子樹,只到達這個節點一次。

當第二次來到這個節點的時候,左子樹的節點必定所有遍歷完。

 

 

使用最右節點的右指針來標記,是第一次來到節點仍是第二次來到。

不是徹底二叉樹也能夠。

 

 

遞歸版本的遍歷,會通過節點三次,打印時機在第一次就是先序,第二次中序,第三次後序。(最本質的是下面的遍歷)

 

 

 

Morris,把第一次來到節點的時候打印,就是先序遍歷。

打印時機放在最後一次來到這個節點的時候,就是中序遍歷,例如一個節點有左子樹,先把左子樹處理完再打印,就是中序,當沒有左子樹的時候就直接打印(第一次和第二次重疊)。

 

怎麼作到後序?

後序遍歷只關心能到本身兩次的節點,第二次到達節點的時候逆序打印左子樹的右邊界,打印完成後,在整個函數退出前,單獨打印整課樹的右邊界,就是後序遍歷。

 

 

 

怎麼逆序打印整棵樹右邊界?

用相似逆轉鏈表的方式,把指針修改,打印完,再調整回去。

 

 

時間複雜度:

每次通過節點都打印右子樹,都是有限的幾回,全部複雜度仍是O(N)

 

 

例子:

整顆左子樹能夠被右邊界分解。每條右邊界被遍歷了兩遍。右邊界總體節點個數N個,被遍歷有限幾遍,一共O(N)。

 

public class Code03_01_MorrisTraversal {

    //幫助理解Morris遍歷
    public static void process(Node head) {
        if (head == null) {
            return;
        }
        // 1
        //System.out.println(head.value);
        process(head.left);
        // 2
        //System.out.println(head.value);
        process(head.right);
        // 3
        //System.out.println(head.value);
    }


    public static class Node {
        public int value;
        Node left;
        Node right;

        public Node(int data) {
            this.value = data;
        }
    }
    //中序遍歷
    public static void morrisIn(Node head) {
        if (head == null) {
            return;
        }
        Node cur = head;
        Node mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {//若是左孩子不等於空
                //找到最右的節點,排除了null和cur的狀況,能一直找下去
                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;
                }
            }
            System.out.print(cur.value + " ");
            cur = cur.right;
        }
        System.out.println();
    }
    //先序遍歷
    public static void morrisPre(Node head) {
        if (head == null) {
            return;
        }
        Node cur = head;
        Node 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;
                    System.out.print(cur.value + " ");
                    cur = cur.left;
                    continue;
                } else {
                    mostRight.right = null;
                }
            } else {//當前節點沒有左子樹的時候,直接打印(中,左)再往右
                System.out.print(cur.value + " ");
            }
            cur = cur.right;
        }
        System.out.println();
    }
    //後序遍歷
    public static void morrisPos(Node head) {
        if (head == null) {
            return;
        }
        Node cur1 = head;
        Node cur2 = null;
        while (cur1 != null) {
            cur2 = cur1.left;
            if (cur2 != null) {
                while (cur2.right != null && cur2.right != cur1) {
                    cur2 = cur2.right;
                }
                if (cur2.right == null) {
                    cur2.right = cur1;
                    cur1 = cur1.left;
                    continue;
                } else {//第二次來的本身的時候
                    cur2.right = null;
                    printEdge(cur1.left);//逆序打印左子樹右邊界
                }
            }
            cur1 = cur1.right;
        }
        printEdge(head);
        System.out.println();
    }

    public static void printEdge(Node head) {
        Node tail = reverseEdge(head);
        Node cur = tail;
        while (cur != null) {
            System.out.print(cur.value + " ");
            cur = cur.right;
        }
        reverseEdge(tail);
    }

    public static Node reverseEdge(Node from) {
        Node pre = null;
        Node next = null;
        while (from != null) {
            next = from.right;
            from.right = pre;
            pre = from;
            from = next;
        }
        return pre;
    }

    // for test -- print tree
    public static void printTree(Node head) {
        System.out.println("Binary Tree:");
        printInOrder(head, 0, "H", 17);
        System.out.println();
    }

    public static void printInOrder(Node head, int height, String to, int len) {
        if (head == null) {
            return;
        }
        printInOrder(head.right, height + 1, "v", len);
        String val = to + head.value + to;
        int lenM = val.length();
        int lenL = (len - lenM) / 2;
        int lenR = len - lenM - lenL;
        val = getSpace(lenL) + val + getSpace(lenR);
        System.out.println(getSpace(height * len) + val);
        printInOrder(head.left, height + 1, "^", len);
    }

    public static String getSpace(int num) {
        String space = " ";
        StringBuffer buf = new StringBuffer("");
        for (int i = 0; i < num; i++) {
            buf.append(space);
        }
        return buf.toString();
    }

    public static void main(String[] args) {
        Node head = new Node(4);
        head.left = new Node(2);
        head.right = new Node(6);
        head.left.left = new Node(1);
        head.left.right = new Node(3);
        head.right.left = new Node(5);
        head.right.right = new Node(7);
        printTree(head);
        morrisIn(head);
        morrisPre(head);
        morrisPos(head);
        printTree(head);
    }

}

 

之後但凡碰見能用遍歷解的面試題,都是扯Morris遍歷,就會讓面試官高看你一眼!(把別人弄掉,你本身上)

相關文章
相關標籤/搜索