数据结构和算法(一)

概念

数据结构:可以容纳数据的结构叫做数据结构。
数据的呈现有很多种形式,比如,线性数据、图形数据、树形数据等等,可以想象数据结构是数据的容器。

算法:是可以对数据结构进行处理的方法。
这就表现在数据量大的时候了,想象一下,如果有非常多的数据,同时数据嵌套很深,在那么多的数据中找自己想要的数据、或者对特定的数据进行操作,这样就很麻烦了。

就像搬家一样,搬家时肯定不会把东西一件一件的往车里放,一定是把东西装在盒子里归置好,打包成一个一个的盒子或包,包里的物品就是数据,而那些装物品用的盒子就是数据结构,而把一盒一盒的物品装运到车上的方法则是算法。

这样的解释就更清晰了,果然,知识是来源于生活的。

数据结构的存储方法

  • 顺序存储:数组
  • 链式存储:链表、树、图等

数据结构的基本操作

访问+遍历 ===》 数据增删改查 ===》线性和非线性

对数据的基本操作就是:访问、遍历数据
访问、遍历归根结底就是对数据进行增删改查
那么怎么增删改查呢?总的来说是两种形式,线性的和非线性的。

  • 线性的:for、while循环为代表
  • 非线性的:递归为代表

那么,数据结构的最终目的就是合理的存储数据,进行更高效的增删改查。

一维数据结构

数组

数组相比都很熟悉了,这里就不赘述了。主要总结一下数组的特点以及优缺点。

数组的特点:

  1. 数组存储在物理空间上是连续的
  2. 数组长度在底层是固定不变的
  3. 数组变量指向数组中的第一个元素

数组的优点:

查询性能高。
也就是我们常用的索引,在操作系统中,数组中的 ’ [ '是表示地址的偏移,也就是说查询的本质是根据地址的偏移量。

数组的缺点:

数组的缺点都是来自于数组的优点。

  1. 因为数组在空间上连续的,所以当数据量大时,空间碎片产生的多了,就导致空间浪费,数据容易存不下。
  2. 因为数组的长度是固定不变的,所以很难对其删除和添加数据。消耗性能。
    添加:一个数组中添加一个数据,如果数组空间不够,则系统先会扩容一倍数组的空间,然后复制一份数组的内容,再把添加的元素加入,这就导致空间开销过大,消耗性能。
    删除:删除数组中的中间数据,因为存储是连续的,所以当被删除数据删除后,其后面所有数据都需要移位补上空缺,牵一发而动全身消耗性能。

链表

链表:是一个有向链式数据。
如果传递一个链表,必须传递链表的根节点,也就是第一个节点。

链表中的每一个节点,都认为自己是根节点。

为什么这么说呢?因为每个节点只知道自己指向谁,并不知道谁指向了自己,所以在自己看来知道下家不知道上家,那自己就是根节点。
链表一般说的都是单向链表,双向链表每个节点多开销两个引用空间,性能不好。

链表的特点:

  1. 链表存储在空间上不是连续的
  2. 链表中的每存放一个节点,都会多开销一块引用空间(指针)

链表的优点:

  1. 只要内存足够大数据就能放下,不用担心空间碎片的问题。
  2. 链表的添加和删除节点很方便,只需将删除节点的上家的next指向删除节点的下家即可。

链表的缺点:

  1. 查询数据的性能低。
  2. 每个节点多开销一个引用空间。但是,当节点中存储的数据越多时,该缺点的体现就大大减弱了
链表的逆置(算法入门题)

思路:
让当前节点等于下一个节点的next,当前节点的next指向null,返回节点,循环递归操作,直到找到倒数第二个节点,返回根节点,递归结束。也就是说,找到根节点就是递归出口。
链表逆置

function invertLink(root){
	// 找到了倒数第二个节点 root的下一个节点的next 指向 null,说明root的下一个节点(root.next)是最后一个节点,也是逆置链表的根节点
	if(root.next.next == null){
		root.next.next = root; // 最后一个节点指向倒数第二个节点root
		return root.next; // 返回根节点
	}else{
		let result = invertLink(root.next);
		root.next.next = root;
		root.next = null;
		return result;
	}
}

二维数据结构

二维数组
形如:[ [] , [] , [] , [] ] 的数组是二维数组。数组中的每一项元素还是一个数组,就是二维数组。

二维拓扑结构(图)

图,也可以说是关系图,只要关系不变,图就是不变的。看下面的图,做个比喻,A的邻居是B和C,反过来C的邻居里除了E也包含A,依次类推,关系是双向的。
图

树:树是图的一种,也叫有向无环图。

所以,树里面是没有环路的,上面的图则不是树。
树
补充树的一些知识:

父节点:节点下面有子节点的节点,比如,C是EF的父节点
子节点:节点上面有节点指向自己,比如:EF是C的子节点
兄弟节点:同一个父节点下的节点是兄弟节点,E的兄弟节点是F,C的兄弟节点是B和D

树则是典型的二维拓扑结构,用代码实现的话,节点的next值就是一个数组了。

	function TreeNode(value) {
	    this.value = value;
	    this.children = [];
	}
	const a = new TreeNode('a');
	const b = new TreeNode('b');
	const c = new TreeNode('c');
	const d = new TreeNode('d');
	const e = new TreeNode('e');
	const f = new TreeNode('f');
	
	a.children.push(b,c,d);
	c.children.push(e,f);
	
	console.log(root);
二叉树

树的度最多是2的树。
二叉树相关专业术语:

因为二叉树是度为2的树,则说明节点最多有两个节点。
左孩子:节点左边的节点,叫左节点也叫左孩子
右孩子:节点右边的节点,叫右节点也叫右孩子

下面的图是二叉树
在这里插入图片描述
再补充一些知识,

在二叉树中,每一个节点都认为自己是根节点。
左子树:C是A的左子树。C是以C为首的左子树的根节点。
右子树:D是A的有子树。D是以D为首的右子树的根节点。

满二叉树

满二叉树的条件:

  1. 所有的叶子节点必须在树的最底层
  2. 所有的非叶子节点必须有左右孩子两个个节点

这个不是满二叉树,根节点的左孩子节点没有两个节点孩子,少一个右孩子。
在这里插入图片描述
这个是,满二叉树,第一,所有的叶子节点在最后一层;第二,除了叶子节点都有两个孩子节点。
在这里插入图片描述

完全二叉树
  1. 所有叶子节点都在最后一层或者倒数第二层
  2. 所有叶子节点都向左聚拢

下图就是一个完全二叉树。如果给D添加一个右孩子,则就不是一个完全二叉树了,因为新添加一个大右孩子的话,该叶子节点是向右聚拢而不是向左聚拢的。
在这里插入图片描述

二叉树的遍历

遍历:遍历是把一个集合中的所有数据依次拿出来输出。

  1. 前序遍历:先根次序遍历。先输出根节点,再输出左子树,再输出右子树
  2. 中序遍历:中根次序遍历。先输出左子树,再输出根节点,再输出右子树
  3. 后序遍历:后跟次序遍历。先输出左子树,再输出右子树,再输出根节点

在这里插入图片描述
二叉树的遍历,核心思想就是遍历根节点的左右子树,如果左子树的根节点还是左子树再次递归,右子树同理,直到节点为空,则递归结束返回空。

代码实现:
代码执行结果就不放了。

	// 创建节点
	function Node(value) {
	    this.value = value;
	    this.left = null;
	    this.right = null;
	}
	
	let a = new Node('a');
	let b = new Node('b');
	let c = new Node('c');
	let d = new Node('d');
	let e = new Node('e');
	let f = new Node('f');
	let g = new Node('g');
	
	a.left = b;
	a.right = c;
	b.left = d;
	b.right = e;
	c.left = f;
	c.right = g;

先序遍历

	function forward(root) {
	    if (root == null) return;
	    console.log(root.value);
	    forward(root.left);
	    forward(root.right);
	}
	// 传入树的根节点
	forward(a);

中序遍历

function middle(root) {
    if (root === null) return;
    middle(root.left); // 二叉树的左子树根节点
    console.log(root.value);
    middle(root.right); // 二叉树的右子树根节点
}

middle(a);

后序遍历

function back(root) {
    if (root === null) return;
    back(root.left); // 二叉树的左子树根节点
    back(root.right); // 二叉树的右子树根节点
    console.log(root.value);
}

back(a);
还原二叉树
  1. 给出先序和中序遍历结果,还原二叉树并给出后序遍历结果
  2. 给出后序和中序遍历结果,还原二叉树并给出前序遍历结果

还是上面的二叉树遍历的题目
前序:ABDECFG
中序:DBEAFCG

理论分析一波:
1.1 前序是先根节点,所以 A是根节点。
1.2 中序是先左子树再根节点,所以中序中A前面的DBE是左子树内容,FCG是右子树内容。接着确定了前序中BDE和CFG分别是左右子树。

2.1 前序中左子树BDE,确定B是左子树根节点。A的左孩子是B。
2.2 接着中序左子树DBE,因为B是左子树根节点,则D是B的左孩子,E是B的右孩子。

3.1 前序中右子树CFG,确定C是右子树根节点。A的右孩子是C。
3.2 接着中序右子树FCG,因为C是右子树根节点,则F是C的左孩子,G是C的右孩子。

整个过程就是先确定根节点,再根据根节点判断左右子树,对于左右子树依然是重复前面的操作,直到找到空节点,返回空,递归结束。

数据准备:

	// 前序和后序 还原二叉树代码实现
	const front = ['a', 'b', 'd', 'e', 'c', 'f', 'g'];
	const middle = ['d', 'b', 'e', 'a', 'f', 'c', 'g'];
	const behind = ['d', 'e', 'b', 'f', 'g', 'c', 'a'];
	
	function Node(value) {
	    this.value = value;
	    this.left = null;
	    this.right = null;
	}

代码实现:

	function restoreBinTree(front,middle){
		// 严谨性判断
		if(front==null || middle==null || front.length==0 || middle.length==0 || front.length!=middle.length) return null;
		let root = new Node(front[0]);
		let index = middle.indexOf(root.value); // 根节点在中序中的位置
		let frontLeft = front.slice(1,index+1); // 先序中的左子树
		let frontRight = front.slice(index+1,front.length); // 先序中的右子树
		
		let middleLeft = middle.slice(0,index); // 中序中的左子树
		let middleRight = middle.slice(index+1,middle.length); // 中序中的右子树
		
		// 如果节点的左孩子不是null,则递归上面的操作
		if(!root.left) root.left = restoreBinTree(frontLeft,middleLeft); // 把先序左子树和中序左子树传入
		// 如果节点的右孩子不是null,则递归
		if(!root.right) root.right = restoreBinTree(frontRight,middleRight); // 传出先序右子树和中序右子树
		
		return root; // 返回节点
	}
	const root = restoreBinTree(front,middle);
	console.log(root);
	console.log(root.left);
	console.log(root.right);

后序:DEBFGCA
中序:DBEAFCG
分析的过程与上面同理,只不过是处理逻辑有一点不同。后序中的最后一个是根节点,同样的后序的左右子树的最后一个也是对应子树的根节点。

直接代码实现:

	function restoreBinTree(behind,middle){
		// 严谨性判断
		if(front==null || middle==null || front.length==0 || middle.length==0 || front.length!=middle.length) return null;
		
		let root = new Node(behind[behind.length-1]);
		let index = middle.indexOf(root.value);
		
		let behindLeft = behind.slice(0,index);
		let behindRight = behind.slice(index,behind.length-1);

		let middleLeft = middle.slice(0,index); // 中序中的左子树
		let middleRight = middle.slice(index+1,middle.length); // 中序中的右子树

		if(!root.left) root.left = restoreBinTree(behindLeft,middleLeft);
		if(!root.right) root.right = restoreBinTree(behindRight,middleRight);

		return root;
	}

	const root = restoreBinTree(behind,middle);
	console.log(root);
	console.log(root.left);
	console.log(root.right);
二叉树的深度优先搜索

深度优先搜索,是一种搜索方式,还有树的搜索、图的搜索,再者有爬虫。
深度优先搜索更适合探索未知。因为它的思想是根据树的深度来实现搜索的。

给出一个我自己画的深度优先搜索的图。
在这里插入图片描述
追加一个问题:
在上面的二叉树中寻找节点 N

那么接着图片中的结果继续搜索,F不是
找F的左孩子,为空不是
返回节点F
找F的右孩子,为空不是
返回节点C
找C的右孩子,G不是
找G的左孩子,为空不是
返回节点G
找G的右孩子,为空不是
返回节点C
返回节点A
至此整个二叉树搜索完毕,没有目标节点
最后返回false

这些知识是关于理论的,只有直到了它们的思想过程,才能更好的去实现代码。
ok,接下来就是代码了

	function deepSearch(root,target){
		//严谨性判断
		if(root == null) return false;
		if(root.value == target) return true;
		//递归左子树,存在则返回true
		let left = deepSearch(root.left,target);
		if(left){
			return true;
		}else{ // 左子树没有目标再递归右子树
			//递归右子树
			let right = deepSearch(root.right,target);
			if(right) return true;
			else return false;
		}
	}
	console.log(deepSearch(a,'f'));
	console.log(deepSearch(a,"n"));

从代码实现可以看出来,二叉树的深度优先搜索与二叉树的先序遍历的顺序是相同的。
均是先查找根节点,再查找根节点的左节点,最后是查找节点的右节点。

二叉树的广度优先搜索

广度优先搜索,更适合探索区域,因为广度优先搜索是通过树的一层一层的搜索,树的第一层、第二层、第三层,直到找到目标节点,到最后一层还没有的话,则不存在。
广度优先
代码实现:

	function breadthSearch(list,target){
		//严谨性判断
		if(list == null || list.length == 0) return false;
		const childrens = []; // 用于存放每一层的节点
		//遍历节点集合
		for(let i = 0; i < list.length; i++){
			// 节点不为空
			if(list[i]!=null){
				// 存在目标节点 返回
				if(list[i].value == target){
					return true;
				}else{ // 不存在则获取并保存当前节点的左右子节点
					childrens.push(list[i].left);
					childrens.push(list[i].right);
				}
			}else{ // 为null返回false
				return false;
			}
		}
		return breadthSearch(childrens,target);
	}
	breathSearch([a],"f");
	breathSearch([a],"n");

树的内容还未写完~~~

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值