行无忌

行无忌 查看完整档案

南昌编辑东华理工大学  |  软件工程 编辑东华理工大学  |  前端小白 编辑 segmentfault.com/u/xingwuji 编辑
编辑

每天进步一点点

个人动态

行无忌 赞了回答 · 3月26日

解决如何让每行最后一个元素的样式不一样?浮动或者flex布局,每行排列的个数不确定,根据屏幕宽度适应

还是就放个核心代码吧,css没有那种判断,你得根据屏幕的宽度来计算每行的个数0-0

    window.onresize = xx(items)   // item是li的集合,这里面必须要移除上次添加的所有last样式,你自己看着写吧。。
    xx(item)  // 外面还要调用一次初始化
    function xx(items) {
        // 获取屏幕宽度
        var devW = document.documentElement.clientWidth
        
        // 获取每行最后一个item的下标
        var index = Math.floor(devW / 51)  // 50:盒子宽度,1:盒子和盒子间的间隔

        // 判断有几排
        for (let i = 1; i < Math.floor(items.length + 1) / index; i++) {
            // 循环给最后一个盒子添加样式
            $(items).eq((index * i) - 1).addClass('last')
        }
    }

关注 6 回答 5

行无忌 赞了文章 · 2019-12-25

win10安装nvm踩坑实录

新单位用的win10系统,现在项目用的node版本是10.15.3,最近有个老的移动端项目要加埋点需求,node版本是4.3.1,需要经常切换node版本,于是安装了nvm。以前用的mac,都是用n做node版本管理的,这次win10系统安装nvm踩了不少坑,最常见的问题就是明明提示成功用nvm安装了node,但是键入node和npm命令提示拒绝访问,百度了很多相似的问题,总算完美解决。现在将安装的完整过程记录如下:

一、安装nvm

下载安装包

下载地址:https://github.com/coreybutle... , 选择第二个nvm-setup.zip。

安装

将下载的文件解压后得到nvm-setup.exe,安装直接点下一步即可,有两个界面需要注意:

1.Select Destination Location/设置nvm路径
安装完成后有个setting.txt文件,此路径相当于该文件中的root。
注意路径不支持空格,不能设为类似"C:Program Files"这种,否则node无法使用,推荐设置为:D:softwarenvm

2.Set Node.js Symlink/设置node.js映射路径
此路径相当于setting.txt文件中的path,该路径会与使用的node版本一一对应 ,根据nvm使用的版本,对应不同的node目录,从而实现node版本切换。
注意:
-此路径同样不支持空格,不能设为类似"C:Program Files"这种,否则当你nvm use node版本时会出现如下:

$ nvm use 10.15.3
exit status 1: 'C:\Program' 一串乱码。。。

-也不能将目录设置成nvm安装路径的上一级,比如: D:software,否则明明提示成功用nvm安装了node,但是当你查看node版本时会出现:'node' 不是内部或外部命令,也不是可运行的程序,推荐设置为:D:softwarenodejs 或 D:softwarenvmnodejs

安装成功后会在系统变量里看到两个配置:
NVM_HOME D:softwarenvm
NVM_SYMLINK D:softwarenodejs

查看nvm版本,出现如下,表示nvm安装成功

$ nvm -v
Running version 1.1.7.

二、nvm安装node

设置node与npm下载源

nvm node_mirror https://npm.taobao.org/mirrors/node/
nvm npm_mirror https://npm.taobao.org/mirrors/npm/

设置完下载源后,会发现settings.txt 文件里的path不见了,不用管他
此时setting.txt文件如下:
root: D:softwarenvm
arch: 64
proxy: none
originalpath: .
originalversion:
node_mirror: https://npm.taobao.org/mirrors/node/
npm_mirror: https://npm.taobao.org/mirrors/npm/

安装node版本

以我安装node10.15.3版本为例:
1.安装node版本

$ nvm install 10.15.3 64-bit

2.查看已安装的node版本

$ nvm list
  * 10.15.3 (Currently using 64-bit executable)

3.使用某个node版本

$ nvm use 10.15.3
Now using node v10.15.3 (64-bit)

如上3步后,键入node和npm命令,就可以查看到当前的node和npm版本了

三、nvm常用命令

nvm install <version> [arch]    // 安装node版本
nvm list             // 查看已安装的node版本
nvm use [version] [arch]      // 使用某个node版本
nvm uninstall <version>      // 卸载node版本

查看原文

赞 3 收藏 1 评论 2

行无忌 评论了文章 · 2019-04-25

看到别人的面试题就忍不住做了一下

失恋了就专心写代码。

1. 设计一个plus函数、实现plus(1)(2)()=3、plus(1)(2)(3)()=6。

今天去深入学习了一下柯里化,写了个柯里化的装饰器,了解请到https://segmentfault.com/a/11...

// 虽然实现了,这并不是好的实现方式,更好见我上面链接
function plus(a){
  function g(b) {
    if(b === undefined) {
      return a
    }
    a+=b;
    return g
  }
  return g
}
  

2. 存在字符串str1、str。找出a是否包含在b中,如果包含多个返回第一个位置,没有包含则返回-1.如:str1='123' str2='4512356123' 则返回2。不能使用indexOf、substr等。

// 复杂度为n-m(str1的长度为n,str2的长度为m)
function t(str1, str2) {
    let a = -1
    if(!str1.length && str1.length > str2.length) {  // 谢谢@JarryChung指出错误
      return a
    }else {
    /**  这是我旧的代码
    let arr_a = str1.split("")
      let arr_b = str2.split("")
      const len = arr_b.length-arr_a.length
      for(let i = 0; i <= len; i++) {  // 参考KMP将复杂度降低m,谢谢 @行政 的建议
        if(arr_b[i] === arr_a[0] && arr_b.slice(i, i + arr_a.length).join("") === str1) {
          a = i
          break
        }
      }
    }
    */
    // 这是 @NrightCc 提出的解法,让我发现了新大陆,跟大家分享一下
        const arr = str2.split(str1);
        if(arr.length>1){
            a = arr[0].length
        }
        
        
        return a
  }
查看原文

行无忌 评论了文章 · 2019-04-16

Vue.js+LeanCloud 单页面博客

之前看了好多关于 Vue.js 的东东,路由哇,状态管理呀,稀里糊涂的一堆东西,每个都相对独立,这些单独的 demo 和教程看起来觉得明白了,揉到一起不好说了就。。所以想结合起来写一写,作为一只前端汪怎么可以没有博客~

写的过程中有一些心得和踩坑,后续会整理出来~

如果觉得有帮助的话,谢谢帮忙 star ^_^

本项目github地址

简介

一个前后端完全分离的单页应用 线上地址点此查看

采用了之前写的 vue.js+LeanCloud(node.js) 的开发样板 github地址点此查看

线上动图如果不显示请点这里

version 1.0 pre-a

  • 回复

  • 引用回复

version 0.1.1

  • 触屏延迟优化

version 0.1

  • 响应式布局

  • 主页,关于,标签

  • 过渡动画

  • 文章显示markdown 和代码高亮

TODO

  • 评论框

  • 触屏优化

技术栈

前端

后端

开发

git clone git@github.com:jiangjiu/vue-leancloud-blog.git
$ cd vue-leancloud-blog
$ npm install

// 启动服务器端, 默认地址 http://localhost:3000
$ lean up

// 另开一个 terminal
$ cd FE
$ npm install
// 启动前端项目,默认地址 http://localhost:8080
$ npm run dev

构建部署

// 在FE目录下  构建前端文件至 /public 文件夹
$ npm run build

// 根目录下 leancloud命令行部署 / 通过 github 部署
$ lean deploy / lean deploy -g

具体部署可参考LeanCloud云引擎 Node.js 部署

License

MIT

查看原文

行无忌 评论了文章 · 2019-04-16

Vue.js+LeanCloud 单页面博客

之前看了好多关于 Vue.js 的东东,路由哇,状态管理呀,稀里糊涂的一堆东西,每个都相对独立,这些单独的 demo 和教程看起来觉得明白了,揉到一起不好说了就。。所以想结合起来写一写,作为一只前端汪怎么可以没有博客~

写的过程中有一些心得和踩坑,后续会整理出来~

如果觉得有帮助的话,谢谢帮忙 star ^_^

本项目github地址

简介

一个前后端完全分离的单页应用 线上地址点此查看

采用了之前写的 vue.js+LeanCloud(node.js) 的开发样板 github地址点此查看

线上动图如果不显示请点这里

version 1.0 pre-a

  • 回复

  • 引用回复

version 0.1.1

  • 触屏延迟优化

version 0.1

  • 响应式布局

  • 主页,关于,标签

  • 过渡动画

  • 文章显示markdown 和代码高亮

TODO

  • 评论框

  • 触屏优化

技术栈

前端

后端

开发

git clone git@github.com:jiangjiu/vue-leancloud-blog.git
$ cd vue-leancloud-blog
$ npm install

// 启动服务器端, 默认地址 http://localhost:3000
$ lean up

// 另开一个 terminal
$ cd FE
$ npm install
// 启动前端项目,默认地址 http://localhost:8080
$ npm run dev

构建部署

// 在FE目录下  构建前端文件至 /public 文件夹
$ npm run build

// 根目录下 leancloud命令行部署 / 通过 github 部署
$ lean deploy / lean deploy -g

具体部署可参考LeanCloud云引擎 Node.js 部署

License

MIT

查看原文

行无忌 赞了文章 · 2019-03-05

自定义标签的编码

customElements.define

在MDN官方文档中,如果你想要使用自定义标签,可以使用customElement类中define方法(IE7以下浏览器不支持),
使用:customElements.define('myselfTagName', myselfClass, extendsObj);
参数:

myselfTagName: 自定义标签名
myselfClass: 自定义标签的类对象,(主要功能在这里实现,一般在自定义标签绑定变量、事件、指令或者是渲染条件)
extendsObj: 内置并继承的元素(包裹着自定义标签的对象,一般不使用,毕竟谁会闲的无聊把基本标签封装成自定义标签然后填充一堆属性,语义化也说不通啊)

attachShadow

官方文档对于shadow DOM解释很模糊,简单的说就是DOM的'一体双魂',拥有同样的函数和方法,但是Shadow DOM比被附加DOM更多的功能,前者具有独自的作用域,并且与外界DOM分隔。(这个作用就是shadow DOM的核心功能,它可以使你编写的DOM与css与其他区域互不影响,相当于vue中css样式<style scoped> </style>的作用);
shadow DOM弥补了customElements缺少隔离作用域(DOM和css作用域)的缺陷。

shadom DOM Root的创建方法: this.attachShadow({mode: 'open'});

this: shadom DOM对象
mode: 开启js调用shadow DOM

代码示范

coding.... 莫道征途路漫漫

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Pop-up info box — web components</title>
  </head>
  <body>
    <h1>Pop-up info widget - web components</h1>

    <form>
      <div>
        <label for="cvc">Enter your CVC <popup-info img="img/alt.png" data-text="Your card validation code (CVC) is an extra security feature — it is the last 3 or 4 numbers on the back of your card."></label>
        <input type="text" id="cvc">
      </div>
    </form>
  </body>
  <script>
/**
 *
 * @description:将shadow root附加到customelement上,
                 然后通过一系列DOM操作创建custom element的内部阴影DOM结构,
                 再将其附加到 shadow root上,最后再将一些CSS附加到 
                 shadow root的style节点上。
 */
 
class PopUpInfo extends HTMLElement {
  constructor() {
    // 必须使用super初始化customElements类的构造器
    super();

    // 创建shadom Dom Root,该Root 挂载在自定义DOM上,该DOM Root节点是一个真实节点。
    const shadow = this.attachShadow({mode: 'open'});

    // Create spans
    const wrapper = document.createElement('span');
    wrapper.setAttribute('class', 'wrapper');

    let icon = document.createElement('span'),
        info = document.createElement('span'),
        text = this.getAttribute('data-text');
          
    icon.setAttribute('class', 'icon');
    icon.setAttribute('tabindex', 0);
    info.textContent = text;
    info.setAttribute('class', 'info');

    let imgUrl = this.hasAttribute('img') ? this.getAttribute('img'): 'img/default.png',
        img = document.createElement('img');
        
    img.src = imgUrl;
    icon.appendChild(img);

    // 优化样式
    const style = document.createElement('style');
    console.log(style.isConnected);

    style.textContent = `
      .wrapper {
        position: relative;
      }

      .info {
        font-size: 0.8rem;
        width: 200px;
        display: inline-block;
        border: 1px solid black;
        padding: 10px;
        background: white;
        border-radius: 10px;
        opacity: 0;
        transition: 0.6s all;
        position: absolute;
        bottom: 20px;
        left: 10px;
        z-index: 3;
      }

      img {
        width: 1.2rem;
      }

      .icon:hover + .info, .icon:focus + .info {
        opacity: 1;
      }
    `;

    // 插入shadow dom Root
    shadow.appendChild(style);
    console.log(style.isConnected);
    shadow.appendChild(wrapper);
    wrapper.appendChild(icon);
    wrapper.appendChild(info);
  }
}
// 自定义标签
customElements.define('popup-info', PopUpInfo);

</script>
</html>

官方示例
MDN CODE

思考

自定义标签的使用减少了我们频繁编写一堆冗余、深层嵌套的代码,提高了速率。然而,如果我们看页面源码会发现customElements.define不会消除自定义标签,自定义标签如果绑定了大量的数据、事件、敏感信息,页面上又完全显示出来,这就增加前端页面的不安全性。如何保证开发者即使用自定义标签又不会编译自定义标签从而导致DOM的过度挂载和数据的泄漏(不建议remove自定义标签,频繁操作DOM太消耗内存了),值得思考...

查看原文

赞 7 收藏 2 评论 0

行无忌 赞了文章 · 2018-08-13

vue全家桶仿某鱼部分布局以及功能实现

前言

每次写文章时,总会觉得比写代码难多了,可能这就是我表述方面的不足吧,然而写文章也是可以复盘一下自己的开发过程,对自己还是受益良多的。在这里简单叙述一下我仿某鱼部分布局以及功能实现的过程,仅做学习用途。

Vue是一套用于构建用户界面的渐进式框架,Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的SPA单页面应用提供驱动。


技术栈以及组件库

  • vuex: 解决组件数据共享问题,加强组件数据持久化。
  • vue-router: 主要实现spa单页面开发。
  • axios: 异步请求数据。
  • easymock: 假数据模拟接口。
  • mint-ui: 一款移动端开发的框架。Mint UI
  • stylus: css预处理器
  • better-scroll: 一个移动端滚动的解决方案
  • swiper: 一个强大的滑动特效插件
  • lrz: 图片压缩插件

实现效果

搜索

分类

登录

购买


发布

代码目录结构

●
┣━ src # 开发目录
 ┣━ api                  //axios获取假数据的统一js
  ┣━ data.js
 ┣━ assets                  //静态文件资源
  ┣━ images                 //图片
  ┣━ utils                  //通用js方法函数
 ┣━ common                 //通用的文件资源
  ┣━ stylus                 //stylus文件夹
 ┣━ component              //可复用的组件
  ┣━...
  ┣━...
  ┣━...
  ┣━...
  ┣━...
  ┣━...
 ┣━ pages                 //页面(页面组件)
  ┣━...
  ┣━...
  ┣━...
  ┣━...
  ┣━...
  ┣━...
 ┣━ router                 //路由
  ┣━ index.js
 ┣━ store                  //vuex数据状态管理
  ┣━ index.js
  ┣━ state.js
  ┣━ mutations.js
  ┣━ actions.js
  ┣━ getters.js
 ┣━ App.vue                //根组件
 ┗━ main.js                 

实现主要的几个功能

  • 登录退出

    用户在已登录状态和未登录状态的界面是不同的,有些功能指定要在登录状态下才会有,因此会产生状态的切换,我们可以通过浏览器自带的window.localStorage来存储数据,也可以用vuex,如果状态多的情况下建议采用vuex

  • 搜索功能

    这个没什么好说的,利用indexOf这个方法来验证假数据中是否有这个key,相应输出它的value,没有那就切换另一个找不到的UI界面

  • 分类功能

    这个也没有什么难度,取到假数据中的数据来for in循环输出,然后用better-scroll插件来实现滚动对应的高度效果

  • 购买

    如果是在未登录的状态下,那么进行一个router-link路由跳转到登录login页面,如果是已登录的状态下,会进入到一个商品详情页,点击我想要会进入到一个与卖家聊天交互的一个界面,这里面的卖家的数据都是模拟出来的假数据,因此它不能像真的卖家一样。其中每一个表情emoji就是一个图片,用code的方式把它编译出来再进行一个swiper轮播来包裹他们的遍历循环。

    接着点击立即购买则会跳到付款页面,如果填过地址等信息的,那么可以选择,如果未填的,则会引导至输入相关信息页面,再点击购买就成功了,这个时候数据就会利用vuex保存到我的个人页面中的我买到的页面中。可以进行确认收货后删除订单。

  • 发布

    一样,只有在已登录的状态下才可以进入到发布的界面,发布就是充当着一个卖家的身份,需要填写商品详情的相关信息包括图片,价格等。通过验证才可发布成功,同样利用了vuex来保存发布的商品信息,再跳至我的页面中的我发布的页面进行数据输出。

  • 设备适配

    我是用rem来实现的,也建议用rem来自适应布局,先自己封装一个自适应html的 font-size的js,再将其导入到main.js中

  /**
    * Created by zhaolele on 2018/7/25.
    */
    (function(doc, win) {
      var docEl = doc.documentElement,
        resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
        recalc = function() {
          var clientWidth = docEl.clientWidth;
          if (!clientWidth) return;
          docEl.style.fontSize = 32 * (clientWidth / 320) + 'px';
        };
      if (!doc.addEventListener) return;
      win.addEventListener(resizeEvt, recalc, false);
      doc.addEventListener('DOMContentLoaded', recalc, false);
    })(document, window);
    
    //10rem
  
  • 移动端一像素

    众所周知,移动端因不同的设备的分辨率导致一像素并不是真正的统一的一像素,因此我们需要封装一个stylus的mixin来引入

border-1px($color)
  position: relative
  &:after
    display: block
    position: absolute
    left: 0
    bottom: 0
    width: 100%
    border-top: 1px solid $color
    content: ' '
    
@media (-webkit-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5)
  .border-1px
    &::after
      -webkit-transform: scaleY(0.7)
      transform: scaleY(0.7)

@media (-webkit-min-device-pixel-ratio: 2),(min-device-pixel-ratio: 2)
  .border-1px
    &::after
      -webkit-transform: scaleY(0.5)
      transform: scaleY(0.5)
  • 图片上传

可以使用input中type的file属性,然后用html5的新属性hidden来隐藏掉这个节点,通过点击其他的节点来触发这个input type=file的点击事件,再利用lrz的图片压缩将图片优美的输出到也页面中。拿里面的上传头像的代码贴一下。

html:
<div class="avatar" @click="addPic">
    <img :data-original="url" alt="">
    <span class="upavatar">上传帅照</span>
    <input type="file" hidden accept="image/jpeg,image/jpg,image/png" capture="camera" @change="fileInput" ref="file">
</div>

js 方法:

addPic() {
  this.$refs.file.click()
},              //点击触发fileInput事件

fileInput(e) {
  let files = e.target.files
  console.log(files)
  if(!files.length) return
  this.createImage(files, e)
},

createImage(files, e) {
  lrz(files[0], { width: 480 }).then(rst=> {
    this.url = rst.base64
  }).catch(err=> {
    console.log(err)
  }).always(()=> {
    e.tartget.value = null
  })
},

结语

**很多东西细节想聊出来,比如嵌套路由所踩的坑..等,但是最近忙于找工作,时间问题就写到这里,有兴趣或者正在学习vue的同学可以查看我的github源码。fallow-fish


如果对你有帮助,可以star我的项目给我一点点的鼓励,也希望有志同道和的可以加入一起讨论,我也会第一时间帮你解答。**

查看原文

赞 258 收藏 200 评论 6

行无忌 发布了文章 · 2018-08-11

用Vue撸一个『A-Z字母滑动检索菜单』

最近用vue仿写途牛旅行APP 遇到了这样的城市列表选择页面,花了些时间,用Vue实现了一下并让它体验的接近 安卓/IOS 原生组件

很多地方都会有这样的侧边栏字母列表菜单,可以滑动实现内容列表联动。

比如手机通讯录城市切换页面都会用到

实现效果

在线体验:http://hx-dl.top/a-z_menu/#/city

技术点

  • better-scroll 实现城市列表滑动
  • evnetBus总线机制实现兄弟组件通信
  • axios请求城市列表数据
  • stylus实现高效css编写
  • 函数节流减少滑动事件开销

页面结构

页面主要由三个子组件拼装而成

t-header

t-list

t-alphabet

  <div class="city">
    <t-header></t-header>
    <t-list :cities="cities" :hotCities="hotCities"></t-list>
    <t-alphabet :cities="cities"></t-alphabet>
  </div>

t-listt-alphabet
子组件的实现直接移步项目源码吧,实现比较简单就不贴代码了,有需要的可以直接拿去用,做点小修改即可

源码地址

Github源码

原文地址:行无忌的成长小屋:用Vue撸一个『A-Z字母滑动检索菜单』

查看原文

赞 42 收藏 33 评论 4

行无忌 关注了专栏 · 2018-08-07

前端每日实战

💯该专栏由《CSS3 艺术》一书的作者亲自维护,已累计分享 170+ 个前端项目从灵感闪现到代码实现的完整过程。💯

关注 5236

行无忌 评论了文章 · 2018-08-05

JavaScript实现简单二叉查找树

前两天接到了蚂蚁金服的面试电话,面试官很直接,上来就抛出了三道算法题。。。

其中有一道关于二叉树实现中序遍历的,当时没回答好,所以特意学习了一把二叉树的知识,行文记录总结。

二叉树&二叉查找树

树相关术语:

节点: 树中的每个元素称为一个节点,

根节点: 位于整棵树顶点的节点,它没有父节点, 如上图 5

子节点: 其他节点的后代

叶子节点: 没有子节点的元素称为叶子节点, 如上图 3 8 24

二叉树:二叉树就是一种数据结构, 它的组织关系就像是自然界中的树一样。官方语言的定义是:是一个有限元素的集合,该集合或者为空、或者由一个称为根的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成。

二叉查找树:
二叉查找树也叫二叉搜索树(BST),它只允许我们在左节点存储比父节点更小的值,右节点存储比父节点更大的值,上图展示的就是一颗二叉查找树。

代码实现

首先创建一个类来表示二叉查找树,它的内部应该有一个Node类,用来创建节点

    function BinarySearchTree () {
        var Node = function(key) {
            this.key = key,
            this.left = null,
            this.right = null
        }
        var root = null
    }

它还应该有一些方法:

  • insert(key) 插入一个新的键
  • inOrderTraverse() 对树进行中序遍历,并打印结果
  • preOrderTraverse() 对树进行先序遍历,并打印结果
  • postOrderTraverse() 对树进行后序遍历,并打印结果
  • search(key) 查找树中的键,如果存在返回true,不存在返回fasle
  • findMin() 返回树中的最小值
  • findMax() 返回树中的最大值
  • remove(key) 删除树中的某个键

向树中插入一个键

向树中插入一个新的键,首页应该创建一个用来表示新节点的Node类实例,因此需要new一下Node类并传入需要插入的key值,它会自动初始化为左右节点为null的一个新节点

然后,需要做一些判断,先判断树是否为空,若为空,新插入的节点就作为根节点,如不为空,调用一个辅助方法insertNode()方法,将根节点和新节点传入

    this.insert = function(key) {
        var newNode = new Node(key)
        if(root === null) {
            root = newNode
        } else {
            insertNode(root, newNode)
        }
    }

定义一下insertNode() 方法,这个方法会通过递归得调用自身,来找到新添加节点的合适位置

    var insertNode = function(node, newNode) {
        if (newNode.key <= node.key) {
            if (node.left === null) {
                node.left = newNode
            }else {
                insertNode(node.left, newNode)
            }
        }else {
            if (node.right === null) {
                node.right = newNode
            }else {
                insertNode(node.right, newNode)
            }
        }
    } 

完成中序遍历方法

要实现中序遍历,我们需要一个inOrderTraverseNode(node)方法,它可以递归调用自身来遍历每个节点

    this.inOrderTraverse = function() {
        inOrderTraverseNode(root)
    }

这个方法会打印每个节点的key值,它需要一个递归终止条件————检查传入的node是否为null,如果不为空,就继续递归调用自身检查node的left、right节点
实现起来也很简单:

    var inOrderTraverseNode = function(node) {
        if (node !== null) {
            inOrderTraverseNode(node.left)
            console.log(node.key)
            inOrderTraverseNode(node.right)
        }
    }

先序遍历、后序遍历

有了中序遍历的方法,只需要稍作改动,就可以实现先序遍历和后序遍历了
上代码:

这样就可以对整棵树进行中序遍历了

    // 实现先序遍历
    this.preOrderTraverse = function() {
        preOrderTraverseNode(root)
    }
    var preOrderTraverseNode = function(node) {
        if (node !== null) {
            console.log(node.key)
            preOrderTraverseNode(node.left)
            preOrderTraverseNode(node.right)
        }
    }

    // 实现后序遍历
    this.postOrderTraverse = function() {
        postOrderTraverseNode(root)
    }
    var postOrderTraverseNode = function(node) {
        if (node !== null) {
            postOrderTraverseNode(node.left)
            postOrderTraverseNode(node.right)
            console.log(node.key)
        }
    }

发现了吧,其实就是内部语句更换了前后位置,这也刚好符合三种遍历规则:先序遍历(根-左-右)、中序遍历(左-根-右)、中序遍历(左-右-根)

先来做个测试吧

现在的完整代码如下:

    function BinarySearchTree () {
        var Node = function(key) {
            this.key = key,
            this.left = null,
            this.right = null
        }
        var root = null
        
        //插入节点
        this.insert = function(key) {
            var newNode = new Node(key)
            if(root === null) {
                root = newNode
            } else {
                insertNode(root, newNode)
            }
        }
        var insertNode = function(node, newNode) {
            if (newNode.key <= node.key) {
                if (node.left === null) {
                    node.left = newNode
                }else {
                    insertNode(node.left, newNode)
                }
            }else {
                if (node.right === null) {
                    node.right = newNode
                }else {
                    insertNode(node.right, newNode)
                }
            }
        } 
        
        //实现中序遍历
        this.inOrderTraverse = function() {
            inOrderTraverseNode(root)
        }
        var inOrderTraverseNode = function(node) {
            if (node !== null) {
                inOrderTraverseNode(node.left)
                console.log(node.key)
                inOrderTraverseNode(node.right)
            }
        }
        // 实现先序遍历
        this.preOrderTraverse = function() {
            preOrderTraverseNode(root)
        }
        var preOrderTraverseNode = function(node) {
            if (node !== null) {
                console.log(node.key)
                preOrderTraverseNode(node.left)
                preOrderTraverseNode(node.right)
            }
        }

        // 实现后序遍历
        this.postOrderTraverse = function() {
            postOrderTraverseNode(root)
        }
        var postOrderTraverseNode = function(node) {
            if (node !== null) {
                postOrderTraverseNode(node.left)
                postOrderTraverseNode(node.right)
                console.log(node.key)
            }
        }
    }

竟然已经完成了添加新节点和遍历的方式,我们来测试一下吧:

定义一个数组,里面有一些元素

var arr = [9,6,3,8,12,15]

我们将arr中的每个元素依此插入到二叉搜索树中,然后打印结果

    var tree = new BinarySearchTree()
    arr.map(item => {
        tree.insert(item)
    })
    tree.inOrderTraverse()
    tree.preOrderTraverse()
    tree.postOrderTraverse()

运行代码后,我们先来看看插入节点后整颗树的情况:

输出结果

中序遍历:

3
6
8
9
12
15

先序遍历:

9
6
3
8
12
15

后序遍历:

3
8
6
15
12
9

很明显,结果是符合预期的,所以,我们用上面的JavaScript代码,实现了对树的节点插入,和三种遍历方法,同时,很明显可以看到,在二叉查找树树种,最左侧的节点的值是最小的,而最右侧的节点的值是最大的,所以二叉查找树可以很方便的拿到其中的最大值和最小值

查找最小、最大值

怎么做呢?其实只需要将根节点传入minNode/或maxNode方法,然后通过循环判断node为左侧(minNode)/右侧(maxNode)的节点为null

实现代码:

    // 查找最小值
    this.findMin = function() {
        return minNode(root)
    }
    var minNode = function(node) {
        if (node) {
            while (node && node.left !== null) {
                node = node.left
            }
            return node.key
        }
        return null
    }
    
    // 查找最大值
    this.findMax = function() {
        return maxNode(root)
    }
    var maxNode = function (node) {
        if(node) {
            while (node && node.right !== null) {
                node =node.right
            }
            return node.key
        }
        return null
    }

所搜特定值

this.search = function(key) {
    return searchNode(root, key)
}

同样,实现它需要定义一个辅助方法,这个方法首先会检验node的合法性,如果为null,直接退出,并返回fasle。如果传入的key比当前传入node的key值小,它会继续递归查找node的左侧节点,反之,查找右侧节点。如果找到相等节点,直接退出,并返回true

    var searchNode = function(node, key) {
        if (node === null) {
            return false
        }
        if (key < node.key) {
            return searchNode(node.left, key)
        }else if (key > node.key) {
            return searchNode(node.right, key)
        }else {
            return true
        }
    }

移除节点

移除节点的实现情况比较复杂,它会有三种不同的情况:

    • 需要移除的节点是一个叶子节点
    • 需要移除的节点包含一个子节点
    • 需要移除的节点包含两个子节点

    和实现搜索指定节点一元,要移除某个节点,必须先找到它所在的位置,因此移除方法的实现中部分代码和上面相同:

        // 移除节点
        this.remove = function(key) {
            removeNode(root,key)
        }
        var removeNode = function(node, key) {
            if (node === null) {
                return null
            }
            if (key < node.key) {
                node.left = removeNode(node.left, key)
                return node
            }else if(key > node.key) {
                node.right = removeNode(node.right,key)
                return node
            }else{
                //需要移除的节点是一个叶子节点
                if (node.left === null && node.right === null) {
                    node = null
                    return node
                }
                //需要移除的节点包含一个子节点
                if (node.letf === null) {
                    node = node.right
                    return node
                }else if (node.right === null) {
                    node = node.left
                    return node
                }
                //需要移除的节点包含两个子节点
                var aux = findMinNode(node.right)
                node.key = aux.key
                node.right = removeNode(node.right, axu.key)
                return node
            }
        }
        var findMinNode = function(node) {
            if (node) {
                while (node && node.left !== null) {
                    node = node.left
                }
                return node
            }
            return null
        }

    其中,移除包含两个子节点的节点是最复杂的情况,它包含左侧节点和右侧节点,对它进行移除主要需要三个步骤:

    1. 需要找到它右侧子树中的最小节点来代替它的位置
    2. 将它右侧子树中的最小节点移除
    3. 将更新后的节点的引用指向原节点的父节点

    有点绕儿,但必须这样,因为删除元素后的二叉搜索树必须保持它的排序性质

    测试删除节点

    tree.remove(8)
    tree.inOrderTraverse()

    打印结果:


    3
    6
    9
    12
    15

    8 这个节点被成功删除了,但是对二叉查找树进行中序遍历依然是保持排序性质的

    到这里,一个简单的二叉查找树就基本上完成了,我们为它实现了,添加、查找、删除以及先中后三种遍历方法

    存在的问题

    但是实际上这样的二叉查找树是存在一些问题的,当我们不断的添加更大/更小的元素的时候,会出现如下情况:

    tree.insert(16)
    tree.insert(17)
    tree.insert(18)

    来看看现在整颗树的情况:

    很容易发现,它是不平衡的,这又会引出平衡树的概念,要解决这个问题,还需要更复杂的实现,例如:AVL树,红黑树 哎,之后再慢慢去学习吧

    关于实现二叉排序树,我也找到慕课网的一系列的视频:Javascript实现二叉树算法,
    内容和上述实现基本一致

    原文链接:行无忌的成长小屋:JavaScript实现简单二叉查找树

    查看原文

    认证与成就

    • 获得 213 次点赞
    • 获得 8 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 7 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    注册于 2018-04-18
    个人主页被 1.2k 人浏览