[leetcode javascript]1029. 两地调度(贪心)和 144. 二叉树的前序遍历(四种方法)、94. 二叉树的中序遍历(递归×2、非递归、莫里斯遍历)、145. 二叉树的后序遍历

目录

1029. 两地调度(贪心)

144. 二叉树的前序遍历

方法一:借助额外函数递归

方法二:递归

方法三:迭代

方法四:莫里斯遍历

94. 二叉树的中序遍历

方法一:递归(借助额外的函数)

方法二:递归

方法三:非递归

方法四:莫里斯遍历

 145. 二叉树的后序遍历

方法一:迭代

方法二:莫里斯遍历

方法三:按后序遍历的思路走(用到栈,Map类)

方法四:按后序的思路走,但是遍历完会让整棵树为空(哭笑.jpg


1029. 两地调度(贪心)

思路来自官方题解
执行用时 :68 ms, 在所有 JavaScript 提交中击败了100.00%的用户
内存消耗 :35 MB, 在所有 JavaScript 提交中击败了100.00%的用户

var twoCitySchedCost = function(costs) {
    costs.sort((o1, o2) => {
        return o1[0]-o1[1]-(o2[0]-o2[1]);
    });
    // console.log(costs);
    let sum = 0;
    let middle = Math.floor(costs.length/2);
    for(let i = 0; i < middle; i++){
        sum += costs[i][0];
    }
    for(let i = middle; i < costs.length; i++){
        sum += costs[i][1];
    }
    return sum;
};

看题解之前:首先肯定是比较哪个费用低就选哪个,但是,后面还有一个“每座城市人数要过半”的条件,我觉得如果第四个人是[20,30](自己修改的),那还是得去第二个城市,尽管去第二个城市的费用比第一个要高,但是突然发现第一个是[10,20](题目本来就这样),那如果第四个人改了数据的情况下,第四个人去第二个城市要30,但第一个人去第二个城市才20,那应该会让第一个人去而不是让第四个人去叭,,,
越想越觉得复杂了

看题解之后:js的传参太厉害了叭...让我有点慌。它是怎么依据变量名来判断我传的是数组变量还是只是一个值的变量......

写完并运行好之后将我刚刚的想法运行了一遍,发现我想多了...
输入[[10,20],[30,200],[400,50],[20,30]]
stdout[ [ 30, 200 ], [ 10, 20 ], [ 20, 30 ], [ 400, 50 ] ]
因为差值都是10,就算他俩互换位置,其实效果一样。如10+30,20+20,费用都是40


144. 二叉树的前序遍历

方法一:借助额外函数递归

执行用时 :60 ms, 在所有 JavaScript 提交中击败了99.45%的用户

内存消耗 :33.9 MB, 在所有 JavaScript 提交中击败了11.40%的用户

var preorderTraversal = function(root) {
    let res = [];
    preOrder(root);
    return res;
    
    function preOrder(node){
        if(node){
            res.push(node.val);
            preOrder(node.left);
            preOrder(node.right);
        }
    }
};

方法二:递归

执行用时 :76 ms, 在所有 JavaScript 提交中击败了68.96%的用户

内存消耗 :34.2 MB, 在所有 JavaScript 提交中击败了5.18%的用户

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function(root, res = []) {
    if(root == null)return [];
    res.push(root.val);
    if(root.left != null){
        preorderTraversal(root.left, res);
    }
    if(root.right != null){
        preorderTraversal(root.right, res);
    }
    return res;
};

方法三:迭代

执行用时 :64 ms, 在所有 JavaScript 提交中击败了98.35%的用户

内存消耗 :34 MB, 在所有 JavaScript 提交中击败了7.26%的用户

和中序完全不同:从根节点开始,每次迭代弹出当前栈顶元素,并将其孩子节点压入栈中,先压右孩子再压左孩子。(抓住后进先出的特点)

先序遍历:根-左-右。用栈的方式就是,先push根,然后pop根,再push右和左。

var preorderTraversal = function(root) {
    if(root == null)return [];
    let res = [];
    let stack = [];
    
    stack.push(root);
    while(stack.length != 0){
        let curr = stack.pop();
        res.push(curr.val);
        if(curr.right != null){
            stack.push(curr.right);
        }
        if(curr.left != null){
            stack.push(curr.left);
        }
    }
    return res;
};

方法四:莫里斯遍历

执行用时 :60 ms, 在所有 JavaScript 提交中击败了99.45%的用户

内存消耗 :34 MB, 在所有 JavaScript 提交中击败了6.74%的用户

算法 图解

算法的思路是从当前节点向下访问先序遍历的前驱节点,每个前驱节点都恰好被访问两次。

首先从当前节点开始,向左孩子走一步然后沿着右孩子一直向下访问,直到到达一个叶子节点(当前节点的中序遍历前驱节点),所以我们更新输出并建立一条伪边 predecessor.right = root 更新这个前驱的下一个点。如果我们第二次访问到前驱节点,由于已经指向了当前节点,我们移除伪边并移动到下一个顶点。

如果第一步向左的移动不存在,就直接更新输出并向右移动。

var preorderTraversal = function(root) {
    if(!root)return [];
    let res = [];
    let node = root;
    while(node != null){
        if(node.left == null){
            res.push(node.val);
            node = node.right;
        }else{
            let predecessor = node.left;
            while((predecessor.right != null) && (predecessor.right != node)){
                predecessor = predecessor.right;
            }
            if(predecessor.right == null){
                res.push(node.val);
                predecessor.right = node;
                node = node.left;
            }else{ //表明predecessor.right == node,清除那个连接
                predecessor.right = null;
                node = node.right;
            }
        }
    }
    return res;
};

【9.28补充】昨晚睡前回忆了一下莫里斯遍历,发现前序和中序都有几个相同的点,1.都会将根节点(相对的根节点,不是指一棵树只有一个的那个根节点)的左子树最右边的的那个节点的右指针(此时为空的)指向对应的根节点;2.只有在左子树不存在时,才会去看右子树。(前序:根左右,中序:左根右)×前序有两处地方看了右子树


94. 二叉树的中序遍历

方法一:递归(借助额外的函数)

执行用时 :56 ms, 在所有 JavaScript 提交中击败了99.52%的用户

内存消耗 :34.1 MB, 在所有 JavaScript 提交中击败了5.23%的用户

// 递归 额外加一个函数递归
var inorderTraversal = function(root) {
    let res = [];
    inOrder(root);
    return res;
    function inOrder(root){
        if(root){
            inOrder(root.left);
            res.push(root.val);
            inOrder(root.right);
        }
    }
};

方法二:递归

执行用时 :68 ms, 在所有 JavaScript 提交中击败了93.88%的用户

内存消耗 :34.1 MB, 在所有 JavaScript 提交中击败了5.23%的用户

// 利用自身的函数递归 可能因为leetcode的缘故,调用给出的函数会稍微慢一些
var inorderTraversal = function(root, res = []) {
    if(!root)return [];
    if(root.left){
        inorderTraversal(root.left, res);
    }
    res.push(root.val);
    if(root.right){
        inorderTraversal(root.right, res);
    }
    return res;
}

方法三:非递归

执行用时 :64 ms, 在所有 JavaScript 提交中击败了97.32%的用户

内存消耗 :34.1 MB, 在所有 JavaScript 提交中击败了5.23%的用户

// 非递归
var inorderTraversal = function(root){
    let res = [];
    let stack = [];
    let curr = root;
    while(curr != null || stack.length != 0){//stack.length != 0 不能写成 stack!=null。会报错
        while(curr != null){
            stack.push(curr);
            curr = curr.left;
        }
        if(stack.length != 0){
            curr = stack.pop();
            res.push(curr.val);
            curr = curr.right;
        }
        
    }
    return res;
} 

方法四:莫里斯遍历

执行用时 :64 ms, 在所有 JavaScript 提交中击败了97.32%的用户

内存消耗 :34.1 MB, 在所有 JavaScript 提交中击败了5.23%的用户

官方题解

使用一种新的数据结构:线索二叉树。方法如下:

Step 1: 将当前节点current初始化为根节点

Step 2: While current不为空,

若current没有左子节点

    a. 将current添加到输出

    b. 进入右子树,亦即, current = current.right

否则

    a. 在current的左子树中,令current成为最右侧节点的右子节点

    b. 进入左子树,亦即,current = current.left 

 例子来源

     X
   /   \
  Y     Z
 / \   / \
A   B C   D

首先,X是根,因此将其初始化为currentX有一个左孩子,所以X它成为X左子树的最右边孩子(X有序遍历的直接前身)。这样X就做成了合适的孩子B,然后current设置为Y。树现在看起来像这样: 

    Y
   / \
  A   B
       \
        X
       / \
     (Y)  Z
         / \
        C   D

(Y)上面提到了Y及其所有子项,由于递归问题而将其省略。无论如何,重要的部分都会列出。现在树已链接回X,遍历继续...

 A
  \
   Y
  / \
(A)  B
      \
       X
      / \
    (Y)  Z
        / \
       C   D

 然后A输出,因为它没有左子节点,然后current返回YA在上一次迭代中将其作为右子节点。在下一次迭代中,Y有两个孩子。但是,循环的双重条件使其在到达自身时停止,这表明它的左子树已被遍历。因此,它会自行打印,并继续其右侧的子树B

B进行打印,然后current变为X,与经过相同的检查过程一样Y,还意识到它的左子树已被遍历,并继续Z。树的其余部分遵循相同的模式。

代码:

var inorderTraversal = function(root){
    let res = [];
    let current = root;
    let pre;
    while(current != null){
        if(current.left == null){
            res.push(current.val);
            current = current.right;
        }else{
            pre = current.left;
            while(pre.right != null){
                pre = pre.right;
            }
            pre.right = current;
            let temp = current;/*这个不懂,current此时还存着左结点??
                                 答:因为这个节点本身就存在左右节点,这是它一开始就有的。
                                    所以即使被挪去其他地方了,它依旧有左右节点。*/
            current = current.left;
            temp.left = null;
        }
    }
    return res;
}

主要利用了current的左结点来进行current的转换。因为树变了之后,此时树的根节点(pre)是等于(曾经的根节点)current的左结点的


 145. 二叉树的后序遍历

方法一:迭代

68 ms 34 MB

后序遍历:左-右-根,如果反过来(镜像)的话:根-右-左,和前序遍历的根-左-右很像,在前序的基础上变一下就好了。

基于前序的迭代算法基础上要变的地方有两个:①curr.right 和curr.left的顺序要互换  ②插入数据的方式,要从res.push(curr.val)变成res.unshift(curr.val),后者是从数组头进行插入

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = function(root) {
    if(!root)return [];
    let res = [];
    let stack = [];
    
    stack.push(root);
    while(stack.length != 0){
        let curr = stack.pop();
        res.unshift(curr.val);//注意,这是从数组的头插入数据
        if(curr.left){
            stack.push(curr.left);
        }
        if(curr.right){
            stack.push(curr.right);
        }
    }
    return res;
};

方法二:莫里斯遍历

执行用时 :60 ms, 在所有 JavaScript 提交中击败了97.81%的用户

内存消耗 :34.1 MB, 在所有 JavaScript 提交中击败了5.74%的用户

思路依旧和方法一一样,即使是莫里斯遍历,也可以将前序稍微调整一下:前序的根左右->根右左->从右到左读->左右根->后序遍历。只需将left->right,right->left,插入数组的方式从push->unshift

var postorderTraversal = function(root) {
     if(!root)return [];
    let res = [];
    let node = root;
    while(node != null){
        if(node.right == null){
            res.unshift(node.val);
            node = node.left;
        }else{
            let predecessor = node.right;
            while((predecessor.left != null) && (predecessor.left != node)){
                predecessor = predecessor.left;
            }
            if(predecessor.left == null){
                res.unshift(node.val);
                predecessor.left = node;
                node = node.right;
            }else{ //表明predecessor.left == node,清除那个连接
                predecessor.left = null;
                node = node.left;
            }
        }
    }
    return res;
};

方法三:按后序遍历的思路走(用到栈,Map类)

执行用时 :76 ms, 在所有 JavaScript 提交中击败了64.34%的用户

内存消耗 :34.2 MB, 在所有 JavaScript 提交中击败了5.74%的用户(内存以0.1mb增加着....

来源:官方题解下的经典评论(c++)

var postorderTraversal = function(root) {
    let res = [];
    let path = [];//栈
    let s = new Map();// 记录已经访问的结点
    
    if(root)path.push(root);
    
    while(path.length != 0){
        let node = path[path.length-1];//c++那版对应的top(),如果js改用pop()的话,后面并没有保管根节点的值,pop会直接是根节点消失
        let leftVisited = Boolean(true),
            rightVisited = Boolean(true);
        // 布尔值不能像下面那么用
        // boolean leftVisited = true,
        //         rightVisited = true;
        // 左右结点判断先后顺序不能互换,因为需要先把右结点放进path中
        if(node.right && !s.has(node.right)){//s.has(node.right)表示检查s有没有node.right这个key
            rightVisited = false;
            path.push(node.right);
        }
        if(node.left && !s.has(node.left)){
            leftVisited = false;
            path.push(node.left);
        }
        if(leftVisited && rightVisited){// 左右结点已经访问过了,才可以访问当前结点
            res.push(node.val);
            s.set(node);
            console.log(s.get(node));//因为上一句set没有插入键值,所以此处显示undefined
            path.pop();// 访问过了,从path中移除
        }
    }
    return res;
};

方法四:按后序的思路走,但是遍历完会让整棵树为空(哭笑.jpg

题解来源 好处:容易记,坏处:遍历完树没了

执行用时 :64 ms, 在所有 JavaScript 提交中击败了95.62%的用户

内存消耗 :34.1 MB, 在所有 JavaScript 提交中击败了5.74%的用户

// 这个方法会让树直接消失...
var postorderTraversal = function(root) {
    let res = [], stack = [];
    while(root || stack.length){
        if(root.left){
            stack.push(root);//记录节点
            root = root.left;
        }else if(root.right){
            stack.push(root);
            root = root.right;
        }else{
            res.push(root.val);
            root = stack.pop();
            // 本来觉得先检测root.left是短路检测,会更快一些,但是忽略了如果root为空,先检测root.left的话会报错
            if(root && root.left)root.left = null;
            else if(root && root.right)root.right = null;
        }
    }
    return res;
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值