手写题目之算法篇

Posted echo_numb

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写题目之算法篇相关的知识,希望对你有一定的参考价值。

  • 二叉树广度遍历
  • 二叉树层序遍历
  • 二叉树深度遍历:前序,中序,后序
  • 二叉树的最大深度
  • 二分查找
  • 路径总和
  • 快速排序
  • 第K大
  • 最长公共前缀
  • 无重复字符的最长子串

树遍历很多先定义一个树型结构。

// 树形结构定义
const tree = {
      data: 1,
      left: {
        data: 2,
        left: {
          data: 4,
          left: {
            data: 8,
          },
          right: {
            data: 9
          }
        },
        right: {
          data: 5,
          left: {
            data: 10,
          },
          right: {
            data: 11
          }
        }
      },
      right: {
        data: 3,
        left: {
          data: 6,
          left: {
            data: 12
          }
        },
        right: {
          data: 7
        }
      }
    };

二叉树广度遍历

广度遍历就是遍历每一层,上面的tree 广度遍历之后的结果是[1,2,3,4,5,6,7,8,9,10,11,12]。实现方式是通过栈保存遍历状态,按父节点,左子树,右子树的顺序压栈。取出栈顶保存到结果数组中,等栈为空时遍历结束。

代码如下:

// 广度遍历
function bfs(tree){
    // 通过栈保存遍历状态
    let stack = [tree];
    let result = [];
    
    while(stack.length){
        // 取出栈顶
        const node = stack.shift();
        
        result.push(node.data);
        // 有左子树压到栈中
        if(node.left){
            stack.push(node.left);
        }
        // 右右子树压到栈中
        if(node.right){
            stack.push(node.right);
        }
    }

    return result;

}

console.log(\'广度遍历\', bfs(tree));

二叉树层序遍历

层序遍历是每个层次保存到一个数组项。

    // 层序遍历
        function levelOrder(tree){
            const output = [];

            let level = 0;

            if(level === null){
                return output;
            }

            const visitLoop = (node, level) => {
                if(!output[level]){
                    output[level] = [];
                }

                output[level].push(node.data);

                if(node.left){
                    visitLoop(node.left, level + 1);
                }

                if(node.right){
                    visitLoop(node.right, level + 1);
                }
            };

            visitLoop(tree, 0);

            return output;

        }

        console.log(\'层序遍历\', levelOrder(tree));

二叉树前序遍历

前序遍历的顺序是先访问根,再访问左子树,再访问右子树。前序遍历的结果是[1, 2, 4, 8, 9, 5, 10, 11, 3, 6, 12, 7]。
利用栈的特性保存当前遍历状态。初始化根节点先压栈。每次循环中取出栈顶保存到结果数组中,如果有右子树压栈,有左子树压栈。

// 前序遍历
function preOrder(tree){
    let stack = [tree];
    let result = [];
    let node;

    while(stack.length){
        let node = stack.pop();

        result.push(node.data);

        if(node.right){
            stack.push(node.right);
        }

        if(node.left){
            stack.push(node.left);
        }

    }

    return result;
}

console.log(\'前序遍历\', preOrder(tree));

前序遍历的递归实现

// 前序遍历递归实现
function preOrderRcs(tree){
    let result = [];
    const visitLoop = function(node){
        result.push(node.data);

        if(node.left){
            result.concat(visitLoop(node.left));
        }

        if(node.right){
            result.concat(visitLoop(node.right));
        }
    };

    visitLoop(tree);

    return result;
}

console.log(\'前序遍历-递归实现\', preOrderRcs(tree));

二叉树中序遍历

中序遍历是先访问左子树,在访问根,最后访问右子树。中序遍历结果为[8, 4, 9, 2, 10, 5, 11, 1, 12, 6, 3, 7]。
中序遍历非递归实现用到了回溯算法。

回溯法(back tracking)(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

非递归遍历还是利用栈保存遍历顺序,加一个变量存储当前遍历节点。
遍历条件:栈不为空或者当前节点不为空
有当前节点存入栈中,依次让左子树入栈,不存在左子树时,从栈顶弹出元素存入结果数组,如果弹出元素有右子树,右子树为当前遍历节点,压栈。

// 中序遍历 需要用到回溯算法
        function inOrder(tree){
            const stack = [];
            const result = [];

            // 当前遍历节点存储
            let node = tree;
            
            while(stack.length || node){
                // 有节点就压栈,并往下找左子树
                if(node){
                    stack.push(node);

                    node = node.left;
                }else{
                    // 没有左子树了,从栈顶弹出
                    const current = stack.pop();
                    // 存到结果数组中
                    result.push(current.data);
                    
                    // 有右子树,当前遍历节点为右子树
                    if(current.right){
                        node = current.right;
                    }
                }
            }

            return result;

        }


        console.log(\'中序遍历\', inOrder(tree));
// 递归实现
function inOrderRcs(tree){
    let result = [];
    
    const visitLoop = function(node){
        if(node.left){
            result.concat(visitLoop(node.left));
        }

        result.push(node.data);

        if(node.right){
            result.concat(visitLoop(node.right));
        }
    };

    visitLoop(tree);

    return result;
}

console.log(\'中序遍历-递归实现\', inOrderRcs(tree));

二叉树后序遍历

先遍历左子树,再遍历右子树,最后访问根。后序遍历结果为 [8, 9, 4, 10, 11, 5, 2, 12, 6, 7, 3, 1]。

除了保存遍历顺序的栈和存储当前遍历节点之外,每个节点加上touched属性。当前节点有左子树时依次遍历压栈,当前节点touched设为left。当前节点有右子树时依次遍历压栈,当前touched设为false。如果遍历到叶子节点或者左右子树都已遍历过,存到结果数组。并把当前遍历节点设为栈顶元素。

// 后序遍历
function postOrder(tree){
    const stack = [tree];
    const result = [];
    let node = tree;

    while(stack.length){
        // 依次把左子树压栈,当前节点标记为访问过左子树
        if(node.left && !node.touched){
            node.touched = \'left\';
            node = node.left;
            stack.push(node);
            continue;

        }

        // 依次把右子树压栈,当前节点标记为访问过右子树
        if(node.right && node.touched !== \'right\'){
            node.touched = \'right\';
            node = node.right;
            stack.push(node);

            continue;
        }


        // 如果是叶子结点或者左右子节点被访问过
        const current = stack.pop();
        result.push(current.data);
        
        // 设置栈顶为下一个访问的元素
        node = stack[stack.length - 1];
    }

    return result;
}

console.log(\'后序遍历\', postOrder(tree))
// 后续遍历-递归
function postOrderRcs(tree){
    let result = [];
    const visitLoop = function(node){

        if(node.left){
            result.concat(visitLoop(node.left));
        }

        if(node.right){
            result.concat(visitLoop(node.right));
        }

        result.push(node.data);
    };

    visitLoop(tree);

    return result;
}

console.log(\'后序遍历-递归实现\', postOrderRcs(tree))

二叉树的最大深度

// 二叉树的最大深度
function maxDepth(tree){

    if(tree === null){
        return 0;
    }

    let res = 0;

    const visitLoop = (node, level) => {
        if(!node){
            return 0;
        }

        if(!node.left && !node.right){
            res = Math.max(res, level);
        }


        if(node.left){
            visitLoop(node.left, level + 1);
        }

        if(node.right){
            visitLoop(node.right, level + 1);
        }
    };

    visitLoop(tree, 1);

    return res;
}

console.log(\'二叉树的最大深度\', maxDepth(depthTree));

二分查找

function binarySearch(nums, target){

    if(!nums.length){
        return -1;
    }

    let start = 0
        let end = nums.length - 1;

        let mid;

        while(start <= end){
            let mid = Math.floor((start + end) /2);

            if(target === nums[mid]){
                return mid;
            }
            if(target < nums[mid]){
                end = mid - 1;
            }

            if(target > nums[mid]){
                start = mid + 1;
            }

        }

        return -1;
}

console.log(\'二分查找\', binarySearch([-1,0,3,5,9,12], 9));

路径总和

var hasPathSum = function(root, targetSum) {
    let result = [];

    const find = (node, sum, stack, result, target) => {
        stack.push(node);
        sum = sum + node.val;
        if(!node.left && !node.right && sum === target){
            result.push([...stack]);
        }

        if(node.left){
            find(node.left, sum, stack, result, target);
        }

        if(node.right){
            find(node.right, sum, stack, result, target);
        }

        stack.pop();
    };

    if(root){
        find(root, 0, [], result, targetSum);
    }

    return !!result.length;
};

快速排序

分两个过程:

  1. 划分:根据partition函数把数组从基准位划分为2部分,基准位之前的比它小,之后的比它大。排序时采用左右两个指针,遍历之后如果碰到左边比基准值大右边的小的值时交换两个值,并返回左边的指针。
  2. 递归:有个主函数quickSortHandler,通过执行partition取到左指针坐标,根据该坐标拆分数组进行递归。终止条件是数组长度。
// 快速排序
function quickSort(nums = []){

    const swap = (nums, i ,j) => {
        const temp = nums[j];
        nums[j] = nums[i];
        nums[i] = temp;
    };


    //划分过程
    const partition = (nums, i, j) => {
        let left = i;
        let right = j;

        let mid = Math.floor((right + left)/2);

        // 左指针比右指针大时
        while(left <= right){
            
            while(nums[left] < nums[mid]){
                left++;
            }

            while(nums[right] > nums[mid]){
                right --;
            }

            if(left <= right){
                swap(nums, left, right);
                left++;
                right --;

            }
        }
        return left;
    };

    // 拆分数组递归调用排序
    const quickSortHandler = (nums, left, right) => {

        let index;

        // 终止条件
        if(nums.length > 1){
            index = partition(nums, left, right);

            if(left < index - 1){
                quickSortHandler(nums, left, index - 1);
            }

            if(index < right){
                quickSortHandler(nums, index, right);
            }
        }

        return nums;

    };

    quickSortHandler(nums, 0, nums.length - 1);

    return nums;
}

console.log(\'快速排序\', quickSort([5,1,1,2,0,0]));

第K大

快速排序之后找出数组中arr.length - k 的元素即可。

最长公共前缀

//最长公共前缀
function longestCommonPrefix(strs = []){
    if(!strs.length){
        return \'\';
    }

    let res = strs[0];

    for(let i = 1; i < strs.length; i++){
        let j = 0;

        for(;j < strs[i].length; j++){
            if(strs[i][j] !== res[j]){
                break;
            }
        }

        res = res.substring(0, j);

        if(!res){
            return \'\';
        }
    }

    return res;    
}

console.log(\'最长公共前缀\', longestCommonPrefix(["flower","flow","flight"]));

无重复字符的最长子串

// 无重复字符的最长子串
function lengthOfLongestSubstring(s){
    let max = 0;
    const arr = [];

    if(!s){
        return \'\';
    }

    for(let i = 0; i< s.length; i++){
        if(arr.find(item => item ===s[i])){
            let index = arr.findIndex(item => item === s[i]);
            arr.splice(0, index + 1);
        }

        arr.push(s[i]);

        max = Math.max(arr.length, max);
    }

    return max;

}
console.log(\'无重复字符的最长子串\', lengthOfLongestSubstring(\'abcabcbb\'));

参考文章:
二叉树遍历
学习javascript数据结构与算法

以上是关于手写题目之算法篇的主要内容,如果未能解决你的问题,请参考以下文章

手写算法实现 之 朴素贝叶斯 Naive Bayes 篇

前端面试题之手写promise

前端面试题之手写代码篇

手写数字识别——基于全连接层和MNIST数据集

计算机视觉(CV)sklearn之分类算法与手写数字识别

扫描算法(SCAN)——磁盘调度管理