node中你不知道的遍历树状结构的数据(深度先序遍历+广度先序遍历)

920 阅读4分钟

前言

对于一个树状结构的数据,如下图(图画的丑):


根节点a是唯一的,子节点b,c,d可以有多个(b,c,d)是同级节点,还有孙节点b1,b2等等的嵌套数据,这就是大致的树状结构数据。 那么如何遍历这样的数据呢? 

  1. 深度遍历 
    • 深度遍历是从纵向的维度去思考问题,就是实现一层嵌套一层的遍历。深度遍历也可分为串行和并行。  
    • 串行深度遍历:从根节点a开始,先是遍历b,再遍历b下的b1,之后再遍历b2,当b的分支被遍历之后,开始遍历c的分支,与此类推。 
    • 并行深度遍历:从根节点开始,同级的b,c,d一起遍历,b节点下的b1,b2也一起被遍历了,同理c的分支也是,同级分支之间是并行遍历的。 
  2. 广度遍历 
    •  广度遍历是从横向的维度去思考问题,关键是使用指针和队列实现遍历。广度遍历也可分为从头部开始和从尾部开始。 
    • 头部开始:先将根节点存在队列中(因为根节点是唯一的)。从根节点a出发,遍历a将a下的所以子节点b,c,d节点存放在队列中,再将指针往后面一位移动即到了队列的第二位,再次存放b节点下的子节点,以此类推。 
    • 尾部开始:以头部刚好相反的过程,指针应该是向前移动一位。 好了,了解了深度和广度那如何在node中使用呢?通过使用删除文件目录例子来更好的学习。在node中,有同步和异步的情况,所以都需要考虑到。

深度先序异步串行删除

function preParallDeep(dir,callback){
	fs.stat(dir,function(err,statObj){
		if(statObj.isFile()){
			fs.unlink(dir,callback);
		}else{
			//files 是目录中文件的名称的数组(不包括 '.''..')
			fs.readdir(dir,function(err,files){
				files=files.map(item=>path.join(dir,item));
				let index=0;
				function next(){		//使用next实现异步函数
					if(index==files.length) return fs.rmdir(dir,callback)
					let current=files[index++];
					preParallDeep(current,next);
				}
				next();
			})
		}
	})
}

深度先序异步并行删除

  • 通过循环+闭包+递归实现 使用闭包是为了解决循环过程中拿到每个子节点。

function preParallDeep(dir,callback){
	fs.stat(dir,function(err,statObj){
		if(statObj.isFile()){
			fs.unlink(dir,callback);
		}else{
			//files 是目录中文件的名称的数组(不包括 '.''..')
			fs.readdir(dir,function(err,files){
				files=files.map(item=>path.join(dir,item));
				if(files.length==0){		//空目录实现删除
					return fs.rmdir(dir,callback);
				}
				for(let i=0;i<files.length;i++){		//循环同级的节点
					(function(i){
						//递归实现删除
						preParallDeep(files[i],function(){	
							//回调函数实现当索引和父节点的长度-1相等则说明子节点以及完成被删除了,那么要删除父节点了
							if(i==files.length-1){		//子节点
								return fs.rmdir(dir,callback)
							}
						});
					})(i)
				}
			})
		}
	})
}

  • 通过递归实现

function preParallDeep(dir,callback){
	fs.stat(dir,function(err,statObj){
		if(statObj.isFile()){
			fs.unlink(dir,callback)
		}else{
			fs.readdir(dir,function(err,files){
				files=files.map(item=>path.join(dir,item));
				let index=0;
				if(files.length===0) return fs.rmdir(dir,callback);
				function done(){
					if(++index==files.length) return fs.rmdir(dir,callback);
				}
				files.forEach(dir=>{
					preParallDeep(dir,done)
				})
			})
		}
	})
}

  • 通过Promise实现

function preParallDeep(dir){
	return new Promise((resolve,reject)=>{
		fs.stat(dir,function(err,statObj){			//fs.stat() 检查文件的存在性
			if(statObj.isFile()){						//判断是否是文件
				fs.unlink(dir,resolve);
			}else{
				fs.readdir(dir,function(err,files){
					files=files.map(item=>preParallDeep(path.join(dir,item)));
					Promise.all(files).then(()=>{
						fs.rmdir(dir,resolve);
					})
				})
			}
		})
	})
}

广度先序同步删除

function white(dir){
	let arr=[dir];
	let index=0;
	let current;
	while(current=arr[index++]){
		let dirs=fs.readdirSync(current);
		dirs=dirs.map(item=>path.join(current,item));
		arr=[...arr,...dirs];
	}
	console.log(arr)
}

广度先序异步删除

function white(dir, callback) {
  let currentDir, index = 0, arr = [dir];
  function remove(dirs) {
		let key=dirs.length-1;
		function removeNext(){
			fs.rmdir(dirs[key--],function(err){
				if(key===0) return callback();
				removeNext();
			})
		}
		removeNext();
  }
  function next(currentDir) {
    fs.stat(currentDir, function (err, statObj) {			//fs.stat() 检查文件的存在性
      if (statObj.isFile()) {						//判断是否是文件
       next(arr[++index]);
      } else {
        fs.readdir(currentDir, function (err, files) {
          if (err) return;
          files = files && files.map(item => path.join(currentDir,item));
          arr = [...arr, ...files];
          if (index === arr.length - 1) return remove(arr);
          next(arr[++index]);
        })
      }
    })
  }
  next(arr[index]);
}

总结

node的fs模块有异步和同步的api,异步和同步的处理方式也不同。在写递归函数先考虑第一层和第二层的关系,这样思路才不会乱,保持最清晰的头脑写代码,有想法可一起交流学习,来个技术思想的碰撞。最后,广度和深度遍历,对于性能一般来说深度会更好一点,广度更容易理解,当然也要看具体业务场景。