数据结构与算法-树

129 阅读13分钟

拓展

线性结构和非线性结构

数据结构分类 数据结构分类.png 线性结构和非线性结构 线性结构和非线性结构.png

  1. 线性结构
    • 其特点是数据元素之间存在一对一的线性关系,即除了第一个和最后一个数据元素之外,其他元素都是首尾相连的;
    • 线性结构有两种不同的存储结构,即顺序存储链式存储结构。顺序存储的线性表称为顺序表,顺序表中的存储元素时连续的,链式存储的线性表称为链表,链表存储的元素不一定是连续的,元素节点中存放元素及其相邻元素的地址信息。
    • 特点是集合中必须存在唯一的一个第一个元素和惟一的一个最后一个元素、除了最后元素外,其他数据都有唯一的后继,除了第一个元素外,其他元素都有唯一的一个前驱
    • 线性结构常见的有:数组、队列、链表和栈;

    线性:可以理解为相关性,即数据之间是相关的,官方说法是数据元素之间存在一对一的线性关系,而数据元素之间不一定是物理地址上的连续才是相关的,线性的,主要看我们如何利用,所以线性结构的存储可以分为顺序存储链式存储

  2. 非线性结构
    • 非线性结构的各个数据元素不再保持在一个线性序列中,每个元素可能与零个或多个其他数据元素发生联系,根据联系的不同,可以分为层次结构群结构
    • 非线性结构包括:二维数组、多维数组、广义表、树结构、图结构等;

    对维数组由多个一维数组组成,多维数组和二维数组中每个数据对应的前后数据没有相应的关系,所以二维数组和多维数组是非线性的;

树.png

树简介

  1. 树是一种非线性结构,树的内容较多,包括BST树、AVL树、Trie树等;二叉树也是其中较常见的类型之一;
  2. 其遵循的规则有:
    • 仅有唯一一个根节点,没有节点则为空树;
    • 除根节点外,每个节点都有且仅有唯一一个父节点;
    • 节点之间不形成闭环;
  3. 相关概念
    • 拥有相同父节点的节点,互称为兄弟节点;
    • 节点的深度:从根节点到该节点所经历的边的个数;
    • 节点的高度:节点到叶节点的最长路径;
    • 树的高度:根节点得到高度;
  4. 树的种类
    • 无序树:树的任意节点的子节点之间没有顺序关系,也称自由树;
    • 有序树:树的任意节点的子节点之间有顺序关系;
      • 二叉树:每个节点最多含有两个子树的树称为二叉树;
        • 完全二叉树:对于二叉树,假设其深度为d(d>1),除第d层外,其他各层的节点树有达到最大值,且d层所有节点从左向右连续且紧密的排列,这样的树称为完全二叉树;
      完全二叉树.png - 满二叉树:所有叶节点都在最底层的完全二叉树,每一层都是最大的节点; 满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树 满二叉树.png - 平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度查不大于1的二叉树; - 排序二叉树(BST树),也成二叉搜索树,有序二叉树;
    • 森林:由m(m>=0)棵互不相交的树的集合称为森林 树的概念.png B、C、D就互称为兄弟节点,其中,节点B的高度为2,节点B的深度为 1,树的高度为3

数组、链表、哈希表优缺点分析

  1. 数组
    • 优点:根据下标值访问效率会很高;
    • 缺点:为了提高查找效率,需要对数据进行排序,生成有序数组;且在插入和删除元素时,需要大量的位移操作(插入到首位和中间位置),效率很低;
  2. 链表
    • 优点:数据的插入和删除效率很高;
    • 缺点:查找效率低,需要从头查找,直到找到目标数据为止;当需要在链表中间插入或删除数据时,效率都不高;
  3. 哈希表
    • 优点:插入、查询、删除效率都很高;
    • 缺点:空间利用率不高,底层使用的数组中很多单元都没有被利用;并且哈希表中的元素是无序的,不能按照固定顺序遍历哈希表中的元素,而且不能快速的找出哈希表中的最值等特殊元素;
  4. 树结构
    • 优点:综合了上述几种数据结构得到优点,且弥补相应的确点,但是效率在某些情况下不一定都比它们高;

搜索方式-深度优先/广度优先

深度优先遍历:

深度优先的遍历遵循递归下去、回溯回来,是以深度优先为基准,先走到最底层,然后回退到上一步的状态继续局部深度优先遍历;

广度优先遍历

广度优先遍历是从根节点开始,沿着树的宽度遍历树的节点,旨在面临一个岔路口时,先把所有的岔路口都记录下来,然后选择其中一个进入,然后将其分路记录下来,然后再返回来进入另一个路口,并把他的分路记录下来,然后再回来进入另一个岔路,继续重复上述步骤;

比较

  1. 数据结构运用方面:
    • DFS使用递归的形式,用到了栈结构,后进先出;
    • BFS用到了队列的形式,先进先出;
  2. 复杂度:
    • 两者复杂度大体一致,不同之处在于遍历的方式与对应问题解决的出发点不同;
    • DFS适合目标明确,而BFS适合大范围寻找;

二叉树

二叉树.png

二叉树简介

  1. 二叉树(binary tree)是n(n>=0)个节点的有限集合,该集合或者为空集,或者由一个根结点和两棵互不相交的、分别称为根结点的左子树(left subtree)和右子树(right subtree)的二叉树组成。

二叉树特点

  1. 每个节点最多只有两颗树,所以二叉树不存在深度大于2的节点;
  2. 二叉树是有序的,其顺序也不能任意颠倒,即时树中某个节点只有一棵子树,也要区分他是左子树还是右子树;
  3. 二叉树有挺多优点:相对于链表来书,二叉树进行查找速度非常快,而相对于数组来说,未二叉树添加或删除元素非常快;

二叉树的性质

  1. 层节点 在二叉树的第i层上最多有2^{i-1}个节点(i>=1);
  2. 总节点 深度为k的二叉树最多有2^{k}-1个节点(k>=1);
  3. 深度 具有n个节点的完全二叉树的深度为[log 2 n]+1;向下取整

二叉树分类

搜索二叉树
  1. 搜索二叉树是一种特殊有序的二叉树,需要满足以下条件
    • 其根节点的左子树不能为空,且左子树上的所有节点的值都要小于它的根节点的值;
    • 其根节点的右子树不能为空,且右子树上的所有节点的值都要大于它的根节点的值;
    • 它的左右子树也都是二叉搜索树;
  2. 判断是否为搜索二叉树 递归版本
    • 通过调用函数,根据当前节点是否在函数的上下界范围内,当判断子节点时,将当前节点值作为边界值传给子函数,即判断判断节点左树是将节点值作为子函数的上界(左子树上所有的值都小于根节点的值),判断节点右树是将节点值作为函数的下界传入函数(右子树上所有的值都大于根节点的值),其他界限的值还是继承上一步(根节点)的值;

function isSBT(tree, min, max) {
  if (!tree) return true;
  return isSBT(tree.left, min, tree.val) && isSBT(tree.right, tree.val, max) && (max > tree.val) && (min < tree.val)
}
isSBT(tree, config.MIN_TREE_NUM, config.MAX_TREE_NUM)

树的搜索

深度优先

深度优先是沿着一条边走到黑,然后再一次返回到出发点,再继续下一边的访问,需要注意的是沿一条边遍历的时候任何节点都有可能是局部根节点,返回到该局部根节点的时候也要在该根节点上进行深度优先遍历;

二叉树的遍历

二叉树的遍历包括前序、中序、后续、层序、垂序遍历等,其都是将二叉树的所有节点都遍历一遍,然后按照不同顺序输出节点的值。
不管是哪种遍历方式,都是从根节点还是访问,不同点在于在不同的事件点输出节点的内容;
前序遍历:「根、左、右」;
中序遍历:「左、根、右」;
后序遍历:「左、右、根」; 有两种方式,递归和迭代方法;其中迭代方法用到了数据结构,不断对用旧值递推新值,栈为先进后出的特点。

树深度遍历.png 需要注意的是局部二叉树的概念,且每个节点都有可能成为局部二叉树的根节点

局部根节点.png

测试数据

const testData = {
  value: 1,
  left:  {
    value: 2,
    left:  { value: 4, left: null, right: null },
    right:  { value: 5, left: null, right: null }
  },
  right:  {
    value: 3,
    left:  { value: 6, left: null, right: null },
    right:  { value: 7, left: null, right: null }
  }
}

前序遍历

访问规则
  1. 访问根节点;
  2. 访问根节点左子树进行先需遍历;
  3. 访问根节点右子树进行先需遍历;

栈的方式;访问规则是「根、左、右」;因此入栈顺序是「右、左、根」,出栈顺序就是「根、左、右」了,其中每个节点都有可能成为局部树的根节点;

先序遍历.webp 先序遍历.png

实现原理

先需遍历就是根节点在左子树的前面,左子树在右子树前面,所以在遍历的过程中,先保存根节点,再保存左子树,最后保存右子树;

代码逻辑
//回调的方式
// const preOrder = (root) => {
//   if(!root) return
//   result.push(root.value);
//   preOrder(root.left)
//   preOrder(root.right)
// }

//栈的方式 - 后进先出 进栈方式为先push根节点的右子树 然后再push根节点的左子树;
//每次需要先把根节点入栈,然后立即出栈,再将根节点的右节点和左节点对入栈,然后再执行出栈的操作,这样就可以保证根节点最先出栈;
const preOrder = (root) => {
  if(!root) return;
  const stack = [root];
  //根节点先入栈
  while(stack.length) {
    const timeVariable = stack.pop();
    //栈顶元素出栈
    result.push(timeVariable.value);
    //保存栈顶元素
    if(timeVariable.right) stack.push(timeVariable.right) //右节点入栈
    if(timeVariable.left) stack.push(timeVariable.left) //左节点入栈
  }
}
测试

先序遍历1.png

let result = [];
// preOrder 函数实现...
preOrder(testData)
console.log(result,'preOrder=========')
/*
[
   8,  2,  1, 5,
  12, 10, 16
] preOrder=========
*/

中序遍历

访问规则

  1. 对根节点左子树进行中序遍历;
  2. 访问根节点
  3. 对根节点右子树进行中序遍历; 中序遍历规则

中序遍历.webp 中序遍历.gif

实现原理

中序遍历就是左子树在根节点的前面,根节点在右子树的前面,所以先遍历所有的左子树(遍历到左子树的最深处)再访问根节点,最后是右子树;

代码逻辑

中序遍历.webp

//回调的方式
// const inOrder = (root) => {
//   if(!root) return
//   inOrder(root.left)
//   result.push(root.value);
//   inOrder(root.right)
// }

//栈的方式 - 后进先出 
//中序遍历的方式为「左、根、右」,因此需要重新构建栈结构
//先将根根节点推入到临时栈中,然后将最深左子树推入到临时栈中,由于左、根、右的规则,先依次将栈中的数据出栈推入到result中,同时将当前的节点的右节点推入到栈中;
const inOrder = (root) => {
  if(!root) return;
  const stack = [];
  let timeVariable_R = root;
  while(timeVariable_R || stack.length){
    while(timeVariable_R) {
      //将所有左子树入栈
      stack.push(timeVariable_R)
      timeVariable_R = timeVariable_R.left
    }
    //到达左子树最深处 
    const timeVariable_S = stack.pop();
    result.push(timeVariable_S.value);
    //出栈元素时局部二叉树的根节点,操作局部根节点后再按照顺序找其右节点
    timeVariable_R = timeVariable_S.right;
    //找到右节点后依旧会以当前局部二叉树为"新二叉树"去执行遍历入栈操作
  }
}

测试

中序遍历.png

let result = []; 
// inOrder 函数实现... 
inOrder(testData) 
console.log(result,'inOrder=========')
/*
[
   1,  2,  5, 8,
  10, 12, 16
] inOrder=========
*/

后续遍历

访问规则

  1. 对根节点左子树进行后续遍历;
  2. 对根节点右子树进行后续遍历;
  3. 访问根节点

实现原理

后续遍历是左子树右子树前面,右子树在根节点前面,所以先后序遍历左子树,然后后续遍历右子树,最后为根节点 后续遍历

后续遍历.webp

代码逻辑

//回调的方式
// const postOrder = (root) => {
//   if(!root) return
//   postOrder(root.left)
//   postOrder(root.right)
//   result.push(root.value);
// }

// 栈方式
// const postOrder = (root) => {
//   if(!root) return
//   //栈的方式 - 后进先出 
//   //后序遍历的方式为「左、右、根」,因此需要重新构建栈结构  
//   const outStack = [];
//   const stack = [root];
//   while(stack.length) {
//     //构建栈顶到栈底 - push的根、左、右顺序的栈数据 outStack
//     const timeVariable = stack.pop();
//     outStack.push(timeVariable)
//     if(timeVariable.left) stack.push(timeVariable.left)
//     if(timeVariable.right) stack.push(timeVariable.right)
//   }
//   while(outStack.length){
//     // 依次取出outStack栈数据 - 左、右、根顺序
//     const timeVariable = outStack.pop();
//     result.push(timeVariable.value)
//   }
// }

// const postOrder = (root) => {
//   if(!root) return
//   let stack = [],
//     timestack = [];
//   while(root || timestack.length){
//     while(root){
//       stack.push(root.value);
//       timestack.push(root);
//       root = root.right;
//     }
//     root = timestack.pop();
//     root = root.left
//   }
//   result = stack.reverse()
// }

const postOrder = (root) => {
  if(!root) return;
  let stack = [],
    newNode = '';
  stack.push(root);
  while(stack.length){
    newNode = stack.pop();
    //栈顶元素出栈
    result.unshift(newNode.value); // 保存栈顶元素
    if(newNode.left){ // 左节点入栈
      stack.push(newNode.left)
    }
    if(newNode.right){ // 左节点入栈
      stack.push(newNode.right)
    }
  }
}

测试

后续遍历.png

let result = []; 
// postOrder 函数实现... 
postOrder(testData) 
console.log(result,'postOrder=========')
/*
[
   1,  5, 2, 10,
  16, 12, 8
] postOrder=========
*/

层序遍历

访问规则

  1. 从根节点访问;
  2. 同级子节点树统计到临时缓存数组中;
  3. 继续递归方式访问子节点的子树;

实现原理

层序遍历属于迭代遍历,需要维护多个临时数组进行缓存临时DOM树,当前小DOM树访问结束后将临时数组推入目标数组中即可; 层序遍历

层序遍历.webp 层序遍历.png

代码逻辑

let levelOrdder = function(root) {
  if(!root) return [];
  let queue = [root], result = [];
  while(queue.length){
    let timeStack = [],queueLength = queue.length;
    for(let i = 0;i<queueLength;i++){
      const timeVariable = queue.shift();
      timeStack.push(timeVariable.value)
      timeVariable.left && queue.push(timeVariable.left)
      timeVariable.right && queue.push(timeVariable.right)
    }
    result.push(timeStack)
  }
  return result
}

测试

const testData = {
  value: 8,
  left:  {
    value: 2,
    left:  { value: 1, left: null, right: null },
    right:  { value: 5, left: null, right: null }
  },
  right:  {
    value: 12,
    left:  { value: 10, left: null, 
      right: { value: 11, left: null, right:null }
    },
    right:  { value: 16, left: null, right: null }
  }
}

console.log(levelOrdder(testData),'levelOrdder')
//[ [ 8 ], [ 2, 12 ], [ 1, 5, 10, 16 ], [ 11 ] ] levelOrdder

二叉树源码实现

基类

树的标准存储元素有:自身值、左节点和右节点;默认左右节点值为null

let Node = function(value){
  this.value = value;
  this.left = null;
  this.right = null;
}

插入元素 .insert

let root = null;

let insertNode = function(node,newNode){
  if(newNode.value > node.value){
    if(node.right == null){
      node.right = newNode
    } else {
      insertNode(node.right,newNode)
    }
  } else if(newNode.value < node.value){
    if(node.left == null){
      node.left = newNode
    } else {
      insertNode(node.left,newNode)
    }
  }
}
this.insert = function(value) {
  let newNode = new Node(value);
  if(root == null){
    root = newNode;
  } else {
    insertNode(root,newNode)
  }
}

遍历 .traverse

let traverse = function(node,cb){
  if(node == null) return;
  // cb(node.value) 8 2 3 9
  traverse(node.left,cb)
  // cb(node.value) 2 3 8 9 
  traverse(node.right,cb)
  cb(node,value) //3 2 9 8
}
this.traverse = function(cb){
  traverse(root,cb)
}

最值 .min .max

let min = function(node){
  if(node == null) return null
  while(node && node.left) node = node.left;
  return node.value
}
let max = function(node){
  if(node == null) return null
  while(node && node.right) node = node.right;
  return node.value
}
this.min = function(){
  return min(root)
}
this.max = function(){
  return max(root)
}

移除节点 .remove

let findMinNode = function(node){
  if(node == null) return null;
  while(node && node.left){
    node = node.left
  }
  return node
}
let removeNode = function(node,value){
  if(node == null) return null;
  if(value > node.value){
    //向右遍历查找
    node.right = removeNode(node.right,value)
    return node
  } else if(value < node.value){
    //向左遍历查找
    node.left = removeNode(node.left,value);
    return node
  } else {
    // value == node.value
    if(node.left == null && node.right == null){
      //叶子节点
      node = null;
      return node
    }
    if(node.left == null && node.right){
      //只有右节点
      return node.right
    }

    if(node.left && node.right == null){
      return node.left
    }

    //左右节点都在 - 查找右节点的最小节点
    let RMinNode = findMinNode(node.right);
    node.value = RMinNode.value
    return node
  }
}
this.remove = function(value){
  root = removeNode(root,value)
}

获取根节点值 .getNode

this.getNode = function(){
  return root
}

获取节点信息

let searchNode = function (node, value) {
  if (node === null) false;
  // console.log(node, "node=========");
  if (node.value > value) {
    // 目标值较小,向左急速查找
    return searchNode(node.left, value);
  } else if (node.value < value) {
    // 目标值较大 继续向右查找
    return searchNode(node.right, value);
  } else {
    // 遍历值和目标值相同 找到
    return node;
  }
};
this.searchNode = function (node, value) {
  return searchNode(node, value);
};

完整代码

let tree = function() {
  let Node = function(value){
    this.value = value;
    this.left = null;
    this.right = null;
  }
  let root = null;

  let insertNode = function(node,newNode){
    if(newNode.value > node.value){
      if(node.right == null){
        node.right = newNode
      } else {
        insertNode(node.right,newNode)
      }
    } else if(newNode.value < node.value){
      if(node.left == null){
        node.left = newNode
      } else {
        insertNode(node.left,newNode)
      }
    }
  }
  this.insert = function(value) {
    let newNode = new Node(value);
    if(root == null){
      root = newNode;
    } else {
      insertNode(root,newNode)
    }
  }

  let traverse = function(node,cb){
    if(node == null) return;
    // cb(node.value) 8 2 3 9
    traverse(node.left,cb)
    // cb(node.value) 2 3 8 9 
    traverse(node.right,cb)
    cb(node,value) //3 2 9 8
  }
  this.traverse = function(cb){
    traverse(root,cb)
  }

  let min = function(node){
    if(node == null) return null
    while(node && node.left) node = node.left;
    return node.value
  }
  let max = function(node){
    if(node == null) return null
    while(node && node.right) node = node.right;
    return node.value
  }
  this.min = function(){
    return min(root)
  }
  this.max = function(){
    return max(root)
  }
  this.getNode = function(){
    return root
  }
  let findMinNode = function(node){
    if(node == null) return null;
    while(node && node.left){
      node = node.left
    }
    return node
  }
  let removeNode = function(node,value){
    if(node == null) return null;
    if(value > node.value){
      //向右遍历查找
      node.right = removeNode(node.right,value)
      return node
    } else if(value < node.value){
      //向左遍历查找
      node.left = removeNode(node.left,value);
      return node
    } else {
      // value == node.value
      if(node.left == null && node.right == null){
        //叶子节点
        node = null;
        return node
      }
      if(node.left == null && node.right){
        //只有右节点
        return node.right
      }

      if(node.left && node.right == null){
        return node.left
      }

      //左右节点都在 - 查找右节点的最小节点
      let RMinNode = findMinNode(node.right);
      node.value = RMinNode.value
      return node
    }
  }
  this.remove = function(value){
    root = removeNode(root,value)
  }
  let searchNode = function (node, value) {
    if (node === null) false;
    // console.log(node, "node=========");
    if (node.value > value) {
      // 目标值较小,向左急速查找
      return searchNode(node.left, value);
    } else if (node.value < value) {
      // 目标值较大 继续向右查找
      return searchNode(node.right, value);
    } else {
      // 遍历值和目标值相同 找到
      return node;
    }
  };
  this.searchNode = function (node, value) {
    return searchNode(node, value);
  };
} 

测试代码

let as = new tree()
as.insert(8)
as.insert(2)
as.insert(3)
as.insert(12)

console.log(as.getNode(),'=========',as.max(),as.min())
as.remove(3)
as.insert(5)
as.insert(1)
as.insert(16)
as.insert(10)
console.log(as.getNode(),'=========',as.max(),as.min())

// 单独
console.log(as.searchNode(target, 8), "=========", as.max(), as.min());
// Node {
//   value: 8,
//   left: Node {
//     value: 2,
//     left: Node { value: 1, left: null, right: null },
//     right: Node { value: 3, left: null, right: [Node] }
//   },
//   right: Node {
//     value: 12,
//     left: Node { value: 10, left: null, right: null },
//     right: Node { value: 16, left: null, right: null }
//   }
// } ========= 16 1

sudo.png

树的转化

二叉树转换为数组

数组转化为二叉树

通过传入的数组得到中间值,然后以中间值为树根进行递归遍历赋值当前树根对的左右节点,递归的过程中,会不断的产生临时根节点,只在首次取中间值时产生的值为树根,其他的都为树根的子节点;

function TreeNode(val) {
  this.value = val;
}

var sortedArrayToBST = function (nums) {
  if (nums.length === 0) {
    return null;
  }
  if (nums.length === 1) {
    return new TreeNode(nums[0]);
  }
  var mid = parseInt(nums.length / 2); // 计算中间位置,数组下标从0开始,所以parseInt取整
  var root = new TreeNode(nums[mid]); // 中间位置的元素作为树根
  root.left = sortedArrayToBST(nums.slice(0, mid)); // 递归生成树的左子树
  root.right = sortedArrayToBST(nums.slice(mid + 1)); // 递归生成树的右子树
  return root; // 递归结束后返回树
};

树转化为二叉树

  1. 步骤
    • 连接所有兄弟节点;
    • 对数中的每一个节点,值保留与第一个节点的连线,其他的删除 -- 保留最长子节点连线;
    • 整棵树顺时针旋转90度 树转换为二叉树.webp

二叉树转化为树;

  1. 步骤
    • 将左孩子的右孩子、右孩子的右孩子……全部连接起来;
    • 所有双亲节点删除与右孩子的连线;
    • 调整一定角度

二叉树转化为树.webp

森林转化为二叉树

  1. 步骤
    • 先将森林中每棵树转化为二叉树;
    • 将二叉树根节点视为兄弟节点连接起来;
    • 调整一定角度

森林转化为二叉树.webp

参考文章

图解数据结构-知心宝贝