前端算法面试题2--树、堆

 树的数据结构

let tree = {
    name: "a",
    children: [
        {
            name: "b",
            children: [
                { name: "d", children: [] },
                { name: "e", children: [] }
            ]
        },
        {
            name: "c",
            children: [
                { name: "f", children: [] },
                { name: "g", children: [] }
            ]
        }
    ]
};
深度优先遍历(DFS--深度使用递归

从根节点出发 ,尽可能深的搜索树的节点
技巧:
    1.访问根节点
    2.对根结点的children进行深度优先遍历(递归)


![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/267bb5de51874ed7aa7f16a48efd8751.png)

const fun1 = (root) => {
    console.log(root.name);
    root.children.forEach(fun1);
}

fun1(tree);

> a b d e c f g

扩展
 root.children.forEach(fun1);
forEach 是 JavaScript 数组的一个方法,它接收一个函数作为参数,并对数组的每一个元素执行这个函数。这个函数被称为 "回调函数",它会接收三个参数:当前元素、当前索引和数组本身。大多数情况下,我们只关心当前元素。

在你的代码中,fun1 就是这个回调函数。当你调用 root.children.forEach(fun1),实际上是在对 root.children 数组的每一个元素调用 fun1 函数。因为 fun1 是一个接收一个参数(在这里是树节点)的函数,所以它可以直接作为 forEach 的参数使用。

更具体一点,当 forEach 在数组的每一个元素上调用 fun1 函数时,它会把当前元素作为参数传给 fun1。所以在 fun1 的函数体中,root 实际上是 root.children 数组的一个元素,也就是一个子节点

这是 JavaScript 的函数式编程特性之一。函数可以作为参数传递给其他函数,也可以作为其他函数的返回值。这使得 JavaScript 能够实现非常强大和灵活的编程模式。

循环遍历里面 只有foreach有这个回调函数的作用么

JavaScript 中的许多数组方法都接受回调函数作为参数,并对数组的每个元素执行这个函数。这些方法包括但不限于:

  • forEach(): 对数组的每个元素执行提供的函数。没有返回值。
  • map(): 对数组的每个元素执行提供的函数,并创建一个新数组,新数组中的元素是原数组中每个元素执行函数的结果。
  • filter(): 对数组的每个元素执行提供的函数,并创建一个新数组,新数组中的元素是原数组中使函数返回真值的元素。
  • reduce(): 对数组的每个元素执行提供的函数(接收两个参数,累加器和当前值),并返回一个单一的输出值。
  • some(): 测试数组中是否至少有一个元素通过由提供的函数实现的测试。
  • every(): 测试数组的所有元素是否都通过由提供的函数实现的测试。

这些都是 JavaScript 的高阶函数,因为它们接收另一个函数作为参数。这是函数式编程的一个重要特性,能够使你的代码更简洁、更易理解。


广度优先遍历(BFS --广度使用队列


从根出发,优先访问离根节点最近的节点. 

技巧:

1.新建一个队列,把根节点入队

2.把队头出队

3.把队头的children挨个入队

4.重复2和3步,直到队列为空为止

const fun2=(root)=>{
    const arr=[root]
    while(arr.length>0){
        const o = arr.shift();
        console.log(o.name)
        o.children.forEach(item=>{
            arr.push(item)
        })    
    }
}
fun2(tree)

//a b c d e f g 

二叉树的数据结构

let binaryTree = {
    value: "A",
    left: {
        value: "B",
        left: {
            value: "D",
            left: null,
            right: null
        },
        right: {
            value: "E",
            left: null,
            right: null
        }
    },
    right: {
        value: "C",
        left: {
            value: "F",
            left: null,
            right: null
        },
        right: {
            value: "G",
            left: null,
            right: null
        }
    }
};

对于你提供的二叉树,我们可以按照先序遍历、中序遍历、和后序遍历的方式进行遍历。这里是结果:

  • 先序遍历(根 - 左 - 右): [ 'A', 'B', 'D', 'E', 'C', 'F', 'G' ]
  • 中序遍历(左 - 根 - 右): [ 'D', 'B', 'E', 'A', 'F', 'C', 'G' ]
  • 后序遍历(左 - 右 - 根): [ 'D', 'E', 'B', 'F', 'G', 'C', 'A' ]

这些遍历结果是基于你提供的二叉树结构,其中 'A' 是根节点,'B' 和 'C' 是 'A' 的左右子节点,以此类推。

二叉树的先序遍历 -左右 (根在前
//递归的方式实现
function preOrderTraversal(root) {
    let result = [];
    
    function traverse(node) {
        if (node !== null) {
            // 访问根节点
            result.push(node.value);
            // 遍历左子树
            traverse(node.left);
            // 遍历右子树
            traverse(node.right);
        }
    }

    traverse(root);
    return result;
}

//使用栈的方式实现
function preOrderTraversal(root) {
    let result = [];
    let stack = [];

    if (root !== null) {
        stack.push(root);
    }

    while (stack.length > 0) {
        let node = stack.pop();  // 弹出栈顶节点
        result.push(node.value);  // 访问该节点

        // 先将右子节点入栈,然后左子节点入栈
        if (node.right !== null) {
            stack.push(node.right);
        }
        if (node.left !== null) {
            stack.push(node.left);
        }
    }

    return result;
}
console.log(preOrderTraversal(tree)); // 输出:[ 'A', 'B', 'D', 'E',...

二叉树的中序遍历--左右  (根在中间
//递归的1实现
function inorderTraversal(root) {
    let result = [];

    function traverse(node) {
        if (node !== null) {
            // 先遍历左子树
            traverse(node.left);
            // 然后访问节点本身
            result.push(node.value);
            // 最后遍历右子树
            traverse(node.right);
        }
    }

    traverse(root);
    return result;
}
//栈的实现
function inorderTraversal(root) {
    let result = [];
    let stack = [];
    let node = root;

    while (node !== null || stack.length > 0) {
        // 将所有左子节点入栈
        while (node !== null) {
            stack.push(node);
            node = node.left;
        }
        // 弹出栈顶节点并访问
        node = stack.pop();
        result.push(node.value);
        // 将指针移动到右子树
        node = node.right;
    }

    return result;
}

二叉树的后序遍历--左右  (根在后面
 

//递归实现
function postOrderTraversal(root) {
    let result = [];

    function traverse(node) {
        if (node !== null) {
            // 遍历左子树
            traverse(node.left);
            // 遍历右子树
            traverse(node.right);
            // 访问根节点
            result.push(node.value);
        }
    }

    traverse(root);
    return result;
}

//栈的实现
function postOrderTraversal(root) {
    let result = [];
    let stack = [root];
    let node;

    while (stack.length > 0) {
        node = stack.pop();

        // 将当前节点的值插入到结果数组的前面
        result.unshift(node.value);

        if (node.left !== null) {
            // 如果左子节点不为空则入栈
            stack.push(node.left);
        }

        if (node.right !== null) {
            // 如果右子节点不为空则入栈
            stack.push(node.right);
        }
    }

    return result;
}

算法题

二叉树的最小深度
function minDepth(root) {
    if (root === null) {
        // 如果根节点为空,那么深度为 0
        return 0;
    }

    if (root.left === null && root.right === null) {
        // 如果根节点没有左子节点和右子节点(也就是根节点是叶子节点),那么深度为 1
        return 1;
    }

    if (root.left === null) {
        // 如果根节点没有左子节点,那么最小深度就是右子树的最小深度 + 1
        return minDepth(root.right) + 1;
    }

    if (root.right === null) {
        // 如果根节点没有右子节点,那么最小深度就是左子树的最小深度 + 1
        return minDepth(root.left) + 1;
    }

    // 如果根节点有左子节点和右子节点,那么最小深度就是左子树和右子树的最小深度的较小值 + 1
    return Math.min(minDepth(root.left), minDepth(root.right)) + 1;
}
二叉树的最大深度
function maxDepth(root) {
    if (root === null) {
        // 如果节点为空(无子节点),返回深度为0
        return 0;
    } else {
        // 计算左子树的最大深度
        let leftDepth = maxDepth(root.left);
        // 计算右子树的最大深度
        let rightDepth = maxDepth(root.right);
        // 返回左子树和右子树中的较大深度 + 1
        return Math.max(leftDepth, rightDepth) + 1;
    }
}
翻转二叉树
function invertBinaryTree(root) {
    if (root === null) {
        return null;
    }

    // 交换左右子树
    [root.left, root.right] = [root.right, root.left];

    // 递归翻转左右子树
    invertBinaryTree(root.left);
    invertBinaryTree(root.right);

    return root;
}
相同的树
function isSameTree(p, q) {
    // 如果p和q都是null,返回true
    if (!p && !q) {
        return true;
    }
    // 如果p和q其中一个是null,或者他们的值不相等,返回false
    if (!p || !q || p.value !== q.value) {
        return false;
    }
    // 递归地比较p和q的左子树和右子树
    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}

堆是什么?

堆都能用树来表示,并且一般树的实现都是利用链表

而二叉树是一种特殊的堆,它用完全二叉树表示,却可以利用数组实现,

平时使用最多的是二叉堆,它可以用完全二叉树表示,二叉堆易用存储,并且便于索引。 

堆的数据结构像树,但是,是通过数组来实现的(不是通过链表:二叉堆)

在堆的实现时,需要注意:

因为是数组,所以父子节点的关系就不需要特殊的结构去维护了,索引之间通过计算就可以得到,省掉了很多麻烦。如果是链表结构,就会复杂很多

完全二叉树要求叶子节点从左往右填满,才能开始填充下一层,这就保证了不需要对数组整体进行大片的移动。这也是随机存储结构(数组)的短板:删除一个元素后,整体往前移动是比较费时的,这个特性也导致堆在删除元素的时候,要把最后一个叶子节点补充到树根节点的缘故

二叉堆 安排到数组里面 如何通过当前下标找到父节点和子节点呢?

左:2*index+1

右:2*index+2

找父: (index-1)/2

算法题

最小堆

class MinHeap {
    constructor() {
        this.heap = [];
    }

    // 插入元素
    insert(val) {
        this.heap.push(val); // 将元素添加到数组末尾
        this.bubbleUp(this.heap.length - 1); // 将元素向上移动到正确位置
    }

    // 删除并返回最小的元素
    extractMin() {
        const min = this.heap[0];
        const end = this.heap.pop();
        if (this.heap.length > 0) {
            this.heap[0] = end;
            this.bubbleDown(0);
        }
        return min;
    }

    // 将元素向上移动到正确位置
    bubbleUp(index) {
        const parentIndex = Math.floor((index - 1) / 2);
        if (this.heap[parentIndex] > this.heap[index]) {
            [this.heap[parentIndex], this.heap[index]] = [this.heap[index], this.heap[parentIndex]];
            this.bubbleUp(parentIndex);
        }
    }

    // 将元素向下移动到正确位置
    bubbleDown(index) {
        const left = 2 * index + 1;
        const right = 2 * index + 2;
        let smallest = index;

        if (left < this.heap.length && this.heap[left] < this.heap[smallest]) {
            smallest = left;
        }

        if (right < this.heap.length && this.heap[right] < this.heap[smallest]) {
            smallest = right;
        }

        if (smallest !== index) {
            [this.heap[smallest], this.heap[index]] = [this.heap[index], this.heap[smallest]];
            this.bubbleDown(smallest);
        }
    }
}

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

临夏_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值