概念
数据结构:可以容纳数据的结构叫做数据结构。
数据的呈现有很多种形式,比如,线性数据、图形数据、树形数据等等,可以想象数据结构是数据的容器。
算法:是可以对数据结构进行处理的方法。
这就表现在数据量大的时候了,想象一下,如果有非常多的数据,同时数据嵌套很深,在那么多的数据中找自己想要的数据、或者对特定的数据进行操作,这样就很麻烦了。
就像搬家一样,搬家时肯定不会把东西一件一件的往车里放,一定是把东西装在盒子里归置好,打包成一个一个的盒子或包,包里的物品就是数据,而那些装物品用的盒子就是数据结构,而把一盒一盒的物品装运到车上的方法则是算法。
这样的解释就更清晰了,果然,知识是来源于生活的。
数据结构的存储方法
- 顺序存储:数组
- 链式存储:链表、树、图等
数据结构的基本操作
访问+遍历 ===》 数据增删改查 ===》线性和非线性
对数据的基本操作就是:访问、遍历数据。
访问、遍历归根结底就是对数据进行增删改查。
那么怎么增删改查呢?总的来说是两种形式,线性的和非线性的。
- 线性的:for、while循环为代表
- 非线性的:递归为代表
那么,数据结构的最终目的就是合理的存储数据,进行更高效的增删改查。
一维数据结构
数组
数组相比都很熟悉了,这里就不赘述了。主要总结一下数组的特点以及优缺点。
数组的特点:
- 数组存储在物理空间上是连续的
- 数组长度在底层是固定不变的
- 数组变量指向数组中的第一个元素
数组的优点:
查询性能高。
也就是我们常用的索引,在操作系统中,数组中的 ’ [ '是表示地址的偏移,也就是说查询的本质是根据地址的偏移量。
数组的缺点:
数组的缺点都是来自于数组的优点。
- 因为数组在空间上连续的,所以当数据量大时,空间碎片产生的多了,就导致空间浪费,数据容易存不下。
- 因为数组的长度是固定不变的,所以很难对其删除和添加数据。消耗性能。
添加:一个数组中添加一个数据,如果数组空间不够,则系统先会扩容一倍数组的空间,然后复制一份数组的内容,再把添加的元素加入,这就导致空间开销过大,消耗性能。
删除:删除数组中的中间数据,因为存储是连续的,所以当被删除数据删除后,其后面所有数据都需要移位补上空缺,牵一发而动全身消耗性能。
链表
链表:是一个有向链式数据。
如果传递一个链表,必须传递链表的根节点,也就是第一个节点。
链表中的每一个节点,都认为自己是根节点。
为什么这么说呢?因为每个节点只知道自己指向谁,并不知道谁指向了自己,所以在自己看来知道下家不知道上家,那自己就是根节点。
链表一般说的都是单向链表,双向链表每个节点多开销两个引用空间,性能不好。
链表的特点:
- 链表存储在空间上不是连续的
- 链表中的每存放一个节点,都会多开销一块引用空间(指针)
链表的优点:
- 只要内存足够大数据就能放下,不用担心空间碎片的问题。
- 链表的添加和删除节点很方便,只需将删除节点的上家的next指向删除节点的下家即可。
链表的缺点:
- 查询数据的性能低。
- 每个节点多开销一个引用空间。但是,当节点中存储的数据越多时,该缺点的体现就大大减弱了
链表的逆置(算法入门题)
思路:
让当前节点等于下一个节点的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为首的右子树的根节点。
满二叉树
满二叉树的条件:
- 所有的叶子节点必须在树的最底层
- 所有的非叶子节点必须有左右孩子两个个节点
这个不是满二叉树,根节点的左孩子节点没有两个节点孩子,少一个右孩子。
这个是,满二叉树,第一,所有的叶子节点在最后一层;第二,除了叶子节点都有两个孩子节点。
完全二叉树
- 所有叶子节点都在最后一层或者倒数第二层
- 所有叶子节点都向左聚拢
下图就是一个完全二叉树。如果给D添加一个右孩子,则就不是一个完全二叉树了,因为新添加一个大右孩子的话,该叶子节点是向右聚拢而不是向左聚拢的。
二叉树的遍历
遍历:遍历是把一个集合中的所有数据依次拿出来输出。
- 前序遍历:先根次序遍历。先输出根节点,再输出左子树,再输出右子树
- 中序遍历:中根次序遍历。先输出左子树,再输出根节点,再输出右子树
- 后序遍历:后跟次序遍历。先输出左子树,再输出右子树,再输出根节点
二叉树的遍历,核心思想就是遍历根节点的左右子树,如果左子树的根节点还是左子树再次递归,右子树同理,直到节点为空,则递归结束返回空。
代码实现:
代码执行结果就不放了。
// 创建节点
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);
还原二叉树
- 给出先序和中序遍历结果,还原二叉树并给出后序遍历结果
- 给出后序和中序遍历结果,还原二叉树并给出前序遍历结果
还是上面的二叉树遍历的题目
前序: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");
树的内容还未写完~~~