html 树形结构_从前端的角度回看数据结构

前言

先说点闲话,重返岗位也有一段时间了,目前整体开发的难度并不大,至少我的part没什么很强的交互性,大多数是数据的处理和展示。前段时间看了一篇关于对于前端社区现象的吐槽引发了我的思考。

《前端吐槽 - 原型链就是个链表, 有啥好研究的, 隔三差五来一下, 走马灯么?》

掘金上大量的概念性文章的获得大量的点赞, 在我看来就是没有掌握正确的学习方法导致, 前端开发社区不成熟的表现, 七八年过去了, 一个社区的初学者或者新手们还是在讨论相同的东西, 这真的是一种悲哀。

其实我写的东西好像大多属于这种炒冷饭的,是属于前端基础,更像是一个自己的学习笔记和心得。估计会的人都不会看的文章哈哈。不过还是得写,毕竟自己还不是大神,帮助自己梳理知识的同时加深对知识的印象,保持输出,才是真正属于自己的东西。不过他这种观念也是非常有价值的,如果想确实为前端社区做点什么,写基础的东西确实拉裤裆。随着自己的水平增强,我一定会更新更加有质量的文章。

至于为什么回归数据结构,因为大学也是学过数据结构与算法,以前的c语言算法题刷了不少,不过三年四年过去,在缺乏练习的情况下感觉现在对算法确实不太敏感了,如那篇文章的作者所说,如果好好看看数据结构,根本不至于this,原型链等知识有这么多不同的理解。

下面就让我结合前端的角度,扒扒数据结构的东西,下面是我结合学习画的一个思维导图。cf493a92dd54558f4edd0cb1ff897095.png

栈与队列

在前端中,栈与队列都是用数组去模拟的。数组的原生方法中有push、pop和shift,对应的就是入栈入队、出栈、出队。代码较简单,就不写出来了。

栈,是一种先进后出的数据结构。有点像在一个箱子里放东西,最先放进去的最晚才拿出来,如图。

8f5e207f3bcb2f54da1d32623b53cf97.png

前端中,比较常见的例子就是我们所说的调用栈。

function func1() {
 func2()
    console.log(1)
}

function func2() {
 func3()
    console.log(2)
}

function func3() {
 console.log(3)
}

func1()

// 打印结果:3,2,1

它是先将func1入栈,在func1中遇见func2再将之入栈,func3同上,然后执行func3后再将func3推出,以此类推。

  • 队列

队列是一种先进先出的数据结构(排队打饭,先排队的人先打饭离开队列)。常见的例子就是异步中的任务队列,我之前写过一篇关于异步的文章——细说异步那些事,所以这里就不再赘述了(懒虫人)。

链表

链表是一个由多个元素组成的列表结构,它的每个元素存储地址是不连续的。每个元素是通过指针指向下一个元素,处于链表末尾的指针则指向null。63acfaebd6096f0a21dcf3f7d73511c9.png

同样的,Js中也是没有这个数据结构,我们可以用对象来模拟该数据结构。

const a = { value: '1' };
const b = { value: '2' };
const c = { value: '3' };
a.next = b;
b.next = c;
c.next = null; // 末尾元素指针指向null

通常,我们对一个数组进行非首尾元素的增删时,每一个元素的下标都会改变,所以我们必须查找到相应的位置进行插入或删除。而链表的优势在于我们对它进行非首尾元素增删时,不需要考虑它的元素位置,只需要改变它指针的指向即可。上面的例子中,如果我在b和c之间插入一个d,就直接将b的next指向d,d的next指向c即可。删除也是类似的,只需要将b的指向直接指向c即可。

const d ={ value: 4 };
b.next = d; 
d.next = c; // 增

b.next = c;
d.next = null; // 删
  • 原型链

前言中,那个文章的作者就提到了原型链的本质就是个链表数据结构,只不过它的节点是各种原型对象以及它的指针用的是__proto__来表示的。

比如,一个实例对象的指向的是对象原型(Object.prototype),对象原型指向的是null

const obj = {}
console.log(obj.__proto__) // 对象原型
console.log(obj.__proto__.__proto__) // null

Js中,Function.prototype和Array.prototype均是指向对象原型的。所以他们的__proto__属性也是Object.prototype。这样一来就感觉清晰明了,十分简单。00580930eef3985d2ddcd9925ebf69cb.png

树是由n(n>=1)个有限结点组成一个具有层次关系的集合,如图:5c5d5864a302e1a63de86a6dbd7ff2d1.png

在Js中,我们常用对象与数组来模拟树形结构(后面的算法代码都是基于这个示例来进行遍历)

const tree = {
    val: 'a',
    children: [{
        val: 'b',
        children: [{
            val: 'c',
            children: []
        }, {
            val: 'd',
            children: []
        }]
    }, {
        val: 'e',
        children: [{
            val: 'f',
            children: []
        }, {
            val: 'g',
            children: []
        }]
    }]
}

这可能是和我们前端最贴切的一个数据结构。因为从入门开始我们就接触到了HTML,谈到HTML,就不得不提到Dom的树形结构。如下图所示,根节点是document,第一层子节点是head和body,第二层子节点title和各种元素标签,第三层。。第四层。。等等。根据不同的文档结构,它所呈现的深度以及子节点也不同。(下面这图我懒得画,直接百度扣的,画质是差了点将就看啦,如侵删)2d9e3a911e5750f9478de7fb686fbe23.png

另外就是我们开发常常见到的树状目录结构和级联选择组件,篇幅有限,我这里就不写了,有兴趣可以去看看类似element或者antd这种已经成熟的前端组件框架的代码。

  • 深度、广度优先遍历

假如前端面试要考树的话,那应该考得比较多的就是树的遍历。下面来写写关于这些树的遍历算法,这里我们用console.log()方法来代表访问节点。

(1)深度优先遍历

const dfs = (root) => {
    console.log(root.val)
    if(root.children.length) {
     root.children.forEach(dfs);
    }
}

dfs(tree) // 打印顺序:a、b、c、d、e、f、g

这段代码是运用了递归的方式对树进行一个深度遍历,先访问子节点的值,再看子节点是否为空,如果不为空则遍历子节点。

(2)广度优先遍历

广度优先遍历稍微复杂一点,我们要结合队列这个数据结构进行实现。这里是先将跟节点入了队,当队列的长度不为0时,出队然后访问该节点,再遍历该节点的子节点,将他们依次入队。重复操作直至队列长度为0.

const bfs = (root) => {
    let queue = [root];
    while(queue.length > 0) {
        let n = queue.shift();
        console.log(n.val)
        n.children.forEach(item => {
            queue.push(item)
        })
    }
}

bfs(tree) //打印顺序为:a,b,e,c,d,f,g
  • 二叉树的先、中、后序遍历

二叉树是一种特殊的树形结构。它的每个节点下的子节点都只有两个(左子节点和右子节点)。除了广度和深度遍历之外,它还有先序遍历,中序遍历和后序遍历。这里的结构要稍微的做个变化,子节点直接用left和right对象来表示。

const binaryTree = {
      val: 'a',
      left: {
          val: 'b',
          left: {
              val: 'c',
              left: null,
              right: null
          },
          right: {
              val: 'd',
              left: null,
              right: null
          }
      },
      right: {
          val: 'e',
          left: {
              val: 'f',
              left: null,
              right: null
          },
          right: {
              val: 'g',
              left: null,
              right: null
          }
      }
}

(1) 先序遍历

所谓先序遍历,就是优先访问一个二叉树的根节点,再访问的左子节点,最后再访问它的右子节点。

const preorder = (root) => {
    if (!root) {
        return
    }
    console.log(root.val)
    preorder(root.left)
    preorder(root.right
}
preorder(binaryTree) // 打印顺序为 a,b,c,d,e,f,g

(2) 中序遍历

所谓中序遍历,就是先访问左子节点,再访问根节点,再访问右子节点。

const inorder = (root) => {
    if(!root) {
        return
    }
    inorder(root.left)
    console.log(root.val)
    inorder(root.right)
}
inorder(binaryTree) // 打印顺序:c,b,d,a,f,e,g

(3) 后序遍历

所谓中序遍历,就是先访问左子节点,再访问右子节点,最后访问根节点。

const preorder = (root) => {
 if(!root) {
        return
    }
    preorder(root.left)
    preorder(root.right)
    console.log(root.val)
}
preorder(binaryTree) // 打印顺序:c,d,b,f,g,e,a

集合

集合的两个重要特点:一、成员是无序的;二,每个成员都只在集合中出现一次。在ES6中,js引入了这一概念。

const set = new Set(); // 集合
  • Set的方法

ES6中,我们是可以通过几个方法可以对集合进行增删查的。没啥好说的直接写代码

const set1 = new Set(); // 集合
set1.add(1)
set1.add(2) // 增 {1, 2}
set1.has(2) // 判断集合中是否有2,true
set1.has(3) // 判断集合中是否有3,false
set1.delete(1) // 删 {2}
  • 去重

去重也是面试官特别喜欢问的。数组去重,最简单的方式就是用集合去去重。

const arr = [1,1,2,3]
const arr2 = Array.from(new Set(arr)) // {1, 2}

字典

和集合一样,它也是一种存储唯一的数据结构,不过它是以键值对的形式来存储,也就是一个key对应一个value。Es6中,我们有Map来表示字典。

  • Map的方法

ES6中,也是有几个方法可以对字典进行增删改的

const dic = new Map(); // 集合

//增、改方法都是set
dic.set("a", 1);
dic.set("b", 2);
dic.set("a", 3); // 改变a的值为3

// 删
dic.delete("b");
dic.clear(); // 清空字典

总结

以上就是我总结的目前前端常用的数据结构。其实数据结构还有图和堆两个,但个人觉得前端用的很少,有兴趣可以看看。数据结构这东西是编程的基本,和前端还是有着千丝万缕的联系,所以不单单是为了面试找到更好的offer,也为了对前端底层知识有更深的理解都不应该忽略它。

b8724d0455f5cbe9e137b90f68252038.png

感兴趣就关注个公众号吧!

点击阅读原文去我的掘金首页点个赞吧!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值