拓展
线性结构和非线性结构
数据结构分类
线性结构和非线性结构
- 线性结构
- 其特点是数据元素之间存在一对一的线性关系,即除了第一个和最后一个数据元素之外,其他元素都是
首尾相连
的; - 线性结构有两种不同的存储结构,即
顺序存储
和链式存储
结构。顺序存储的线性表称为顺序表
,顺序表中的存储元素时连续的,链式存储的线性表称为链表
,链表存储的元素不一定是连续的,元素节点中存放元素及其相邻元素的地址信息。 - 特点是集合中必须存在唯一的一个
第一个元素
和惟一的一个最后一个元素
、除了最后元素外,其他数据都有唯一的后继
,除了第一个元素外,其他元素都有唯一的一个前驱
- 线性结构常见的有:数组、队列、链表和栈;
线性:可以理解为
相关性
,即数据之间是相关的,官方说法是数据元素之间存在一对一的线性关系
,而数据元素之间不一定是物理地址上的连续才是相关的,线性的,主要看我们如何利用,所以线性结构的存储可以分为顺序存储
和链式存储
; - 其特点是数据元素之间存在一对一的线性关系,即除了第一个和最后一个数据元素之外,其他元素都是
- 非线性结构
- 非线性结构的各个数据元素不再保持在一个线性序列中,每个元素可能与
零个或多个
其他数据元素发生联系,根据联系的不同,可以分为层次结构
和群结构
; - 非线性结构包括:二维数组、多维数组、广义表、树结构、图结构等;
对维数组由多个一维数组组成,多维数组和二维数组中每个数据对应的前后数据没有相应的关系,所以二维数组和多维数组是非线性的;
- 非线性结构的各个数据元素不再保持在一个线性序列中,每个元素可能与
树
树简介
- 树是一种非线性结构,树的内容较多,包括BST树、AVL树、Trie树等;二叉树也是其中较常见的类型之一;
- 其遵循的规则有:
- 仅有唯一一个根节点,没有节点则为空树;
- 除根节点外,每个节点都有且仅有唯一一个父节点;
- 节点之间不形成闭环;
- 相关概念
- 拥有相同父节点的节点,互称为兄弟节点;
- 节点的深度:从根节点到该节点所经历的边的个数;
- 节点的高度:节点到叶节点的最长路径;
- 树的高度:根节点得到高度;
- 树的种类
- 无序树:树的任意节点的子节点之间没有顺序关系,也称自由树;
- 有序树:树的任意节点的子节点之间有顺序关系;
- 二叉树:每个节点最多含有两个子树的树称为二叉树;
- 完全二叉树:对于二叉树,假设其深度为d(d>1),除第d层外,其他各层的节点树有达到最大值,且d层所有节点从左向右连续且紧密的排列,这样的树称为完全二叉树;
- 满二叉树:所有叶节点都在最底层的完全二叉树,每一层都是最大的节点; 满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树
- 平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度查不大于1的二叉树; - 排序二叉树(BST树),也成二叉搜索树,有序二叉树;
- 二叉树:每个节点最多含有两个子树的树称为二叉树;
- 森林:由m(m>=0)棵互不相交的树的集合称为森林
B、C、D就互称为兄弟节点,其中,节点B的高度为2,节点B的深度为 1,树的高度为3
数组、链表、哈希表优缺点分析
- 数组
- 优点:根据下标值访问效率会很高;
- 缺点:为了提高查找效率,需要对数据进行排序,生成有序数组;且在插入和删除元素时,需要大量的位移操作(插入到首位和中间位置),效率很低;
- 链表
- 优点:数据的插入和删除效率很高;
- 缺点:查找效率低,需要从头查找,直到找到目标数据为止;当需要在链表中间插入或删除数据时,效率都不高;
- 哈希表
- 优点:插入、查询、删除效率都很高;
- 缺点:空间利用率不高,底层使用的数组中很多单元都没有被利用;并且哈希表中的元素是无序的,不能按照固定顺序遍历哈希表中的元素,而且不能快速的找出哈希表中的最值等特殊元素;
- 树结构
- 优点:综合了上述几种数据结构得到优点,且弥补相应的确点,但是效率在某些情况下不一定都比它们高;
搜索方式-深度优先/广度优先
深度优先遍历:
深度优先的遍历遵循
递归下去、回溯回来
,是以深度优先为基准,先走到最底层,然后回退到上一步的状态继续局部深度优先遍历;
广度优先遍历
广度优先遍历是从根节点开始,沿着树的宽度遍历树的节点,旨在面临一个岔路口时,先把所有的岔路口都记录下来,然后选择其中一个进入,然后将其分路记录下来,然后再返回来进入另一个路口,并把他的分路记录下来,然后再回来进入另一个岔路,继续重复上述步骤;
比较
- 数据结构运用方面:
- DFS使用递归的形式,用到了栈结构,后进先出;
- BFS用到了队列的形式,先进先出;
- 复杂度:
- 两者复杂度大体一致,不同之处在于遍历的方式与对应问题解决的出发点不同;
- DFS适合目标明确,而BFS适合大范围寻找;
二叉树
二叉树简介
- 二叉树(binary tree)是n(n>=0)个节点的有限集合,该集合或者为空集,或者由一个根结点和两棵互不相交的、分别称为根结点的左子树(left subtree)和右子树(right subtree)的二叉树组成。
二叉树特点
- 每个节点最多只有两颗树,所以二叉树不存在深度大于2的节点;
- 二叉树是有序的,其顺序也不能任意颠倒,即时树中某个节点只有一棵子树,也要区分他是左子树还是右子树;
- 二叉树有挺多优点:相对于链表来书,二叉树进行查找速度非常快,而相对于数组来说,未二叉树添加或删除元素非常快;
二叉树的性质
- 层节点 在二叉树的第i层上最多有2^{i-1}个节点(i>=1);
- 总节点 深度为k的二叉树最多有2^{k}-1个节点(k>=1);
- 深度 具有n个节点的完全二叉树的深度为[log 2 n]+1;向下取整
二叉树分类
搜索二叉树
- 搜索二叉树是一种特殊有序的二叉树,需要满足以下条件
- 其根节点的左子树不能为空,且左子树上的所有节点的值都要小于它的根节点的值;
- 其根节点的右子树不能为空,且右子树上的所有节点的值都要大于它的根节点的值;
- 它的左右子树也都是二叉搜索树;
- 判断是否为搜索二叉树
递归版本
- 通过调用函数,根据当前节点是否在函数的上下界范围内,当判断子节点时,将当前节点值作为边界值传给子函数,即判断判断节点左树是将节点值作为子函数的上界(左子树上所有的值都小于根节点的值),判断节点右树是将节点值作为函数的下界传入函数(右子树上所有的值都大于根节点的值),其他界限的值还是继承上一步(根节点)的值;
function isSBT(tree, min, max) {
if (!tree) return true;
return isSBT(tree.left, min, tree.val) && isSBT(tree.right, tree.val, max) && (max > tree.val) && (min < tree.val)
}
isSBT(tree, config.MIN_TREE_NUM, config.MAX_TREE_NUM)
树的搜索
深度优先
深度优先是沿着一条边走到黑,然后再一次返回到出发点,再继续下一边的访问,需要注意的是沿一条边遍历的时候任何节点都有可能是局部根节点,返回到该局部根节点的时候也要在该根节点上进行深度优先遍历;
二叉树的遍历
二叉树的遍历包括前序、中序、后续、层序、垂序遍历等,其都是将二叉树的所有节点都遍历一遍,然后按照不同顺序输出节点的值。
不管是哪种遍历方式,都是从根节点还是访问,不同点在于在不同的事件点输出节点的内容;
前序遍历:「根、左、右」;
中序遍历:「左、根、右」;
后序遍历:「左、右、根」; 有两种方式,递归和迭代方法;其中迭代方法用到了栈
数据结构,不断对用旧值递推新值,栈为先进后出
的特点。
需要注意的是局部二叉树的概念,且每个节点都有可能成为局部二叉树的根节点
测试数据
const testData = {
value: 1,
left: {
value: 2,
left: { value: 4, left: null, right: null },
right: { value: 5, left: null, right: null }
},
right: {
value: 3,
left: { value: 6, left: null, right: null },
right: { value: 7, left: null, right: null }
}
}
前序遍历
访问规则
- 访问根节点;
- 访问根节点左子树进行先需遍历;
- 访问根节点右子树进行先需遍历;
栈的方式;访问规则是「根、左、右」;因此入栈顺序是「右、左、根」,出栈顺序就是「根、左、右」了,其中每个节点都有可能成为局部树的根节点;
实现原理
先需遍历就是根节点在左子树的前面,左子树在右子树前面,所以在遍历的过程中,先保存根节点,再保存左子树,最后保存右子树;
代码逻辑
//回调的方式
// const preOrder = (root) => {
// if(!root) return
// result.push(root.value);
// preOrder(root.left)
// preOrder(root.right)
// }
//栈的方式 - 后进先出 进栈方式为先push根节点的右子树 然后再push根节点的左子树;
//每次需要先把根节点入栈,然后立即出栈,再将根节点的右节点和左节点对入栈,然后再执行出栈的操作,这样就可以保证根节点最先出栈;
const preOrder = (root) => {
if(!root) return;
const stack = [root];
//根节点先入栈
while(stack.length) {
const timeVariable = stack.pop();
//栈顶元素出栈
result.push(timeVariable.value);
//保存栈顶元素
if(timeVariable.right) stack.push(timeVariable.right) //右节点入栈
if(timeVariable.left) stack.push(timeVariable.left) //左节点入栈
}
}
测试
let result = [];
// preOrder 函数实现...
preOrder(testData)
console.log(result,'preOrder=========')
/*
[
8, 2, 1, 5,
12, 10, 16
] preOrder=========
*/
中序遍历
访问规则
- 对根节点左子树进行中序遍历;
- 访问根节点
- 对根节点右子树进行中序遍历; 中序遍历规则
实现原理
中序遍历就是左子树在根节点的前面,根节点在右子树的前面,所以先遍历所有的左子树(遍历到左子树的最深处)再访问根节点,最后是右子树;
代码逻辑
//回调的方式
// const inOrder = (root) => {
// if(!root) return
// inOrder(root.left)
// result.push(root.value);
// inOrder(root.right)
// }
//栈的方式 - 后进先出
//中序遍历的方式为「左、根、右」,因此需要重新构建栈结构
//先将根根节点推入到临时栈中,然后将最深左子树推入到临时栈中,由于左、根、右的规则,先依次将栈中的数据出栈推入到result中,同时将当前的节点的右节点推入到栈中;
const inOrder = (root) => {
if(!root) return;
const stack = [];
let timeVariable_R = root;
while(timeVariable_R || stack.length){
while(timeVariable_R) {
//将所有左子树入栈
stack.push(timeVariable_R)
timeVariable_R = timeVariable_R.left
}
//到达左子树最深处
const timeVariable_S = stack.pop();
result.push(timeVariable_S.value);
//出栈元素时局部二叉树的根节点,操作局部根节点后再按照顺序找其右节点
timeVariable_R = timeVariable_S.right;
//找到右节点后依旧会以当前局部二叉树为"新二叉树"去执行遍历入栈操作
}
}
测试
let result = [];
// inOrder 函数实现...
inOrder(testData)
console.log(result,'inOrder=========')
/*
[
1, 2, 5, 8,
10, 12, 16
] inOrder=========
*/
后续遍历
访问规则
- 对根节点左子树进行后续遍历;
- 对根节点右子树进行后续遍历;
- 访问根节点
实现原理
后续遍历是左子树右子树前面,右子树在根节点前面,所以先后序遍历左子树,然后后续遍历右子树,最后为根节点 后续遍历
代码逻辑
//回调的方式
// const postOrder = (root) => {
// if(!root) return
// postOrder(root.left)
// postOrder(root.right)
// result.push(root.value);
// }
// 栈方式
// const postOrder = (root) => {
// if(!root) return
// //栈的方式 - 后进先出
// //后序遍历的方式为「左、右、根」,因此需要重新构建栈结构
// const outStack = [];
// const stack = [root];
// while(stack.length) {
// //构建栈顶到栈底 - push的根、左、右顺序的栈数据 outStack
// const timeVariable = stack.pop();
// outStack.push(timeVariable)
// if(timeVariable.left) stack.push(timeVariable.left)
// if(timeVariable.right) stack.push(timeVariable.right)
// }
// while(outStack.length){
// // 依次取出outStack栈数据 - 左、右、根顺序
// const timeVariable = outStack.pop();
// result.push(timeVariable.value)
// }
// }
// const postOrder = (root) => {
// if(!root) return
// let stack = [],
// timestack = [];
// while(root || timestack.length){
// while(root){
// stack.push(root.value);
// timestack.push(root);
// root = root.right;
// }
// root = timestack.pop();
// root = root.left
// }
// result = stack.reverse()
// }
const postOrder = (root) => {
if(!root) return;
let stack = [],
newNode = '';
stack.push(root);
while(stack.length){
newNode = stack.pop();
//栈顶元素出栈
result.unshift(newNode.value); // 保存栈顶元素
if(newNode.left){ // 左节点入栈
stack.push(newNode.left)
}
if(newNode.right){ // 左节点入栈
stack.push(newNode.right)
}
}
}
测试
let result = [];
// postOrder 函数实现...
postOrder(testData)
console.log(result,'postOrder=========')
/*
[
1, 5, 2, 10,
16, 12, 8
] postOrder=========
*/
层序遍历
访问规则
- 从根节点访问;
- 同级子节点树统计到临时缓存数组中;
- 继续递归方式访问子节点的子树;
实现原理
层序遍历属于迭代遍历,需要维护多个临时数组进行缓存临时DOM树,当前小DOM树访问结束后将临时数组推入目标数组中即可; 层序遍历
代码逻辑
let levelOrdder = function(root) {
if(!root) return [];
let queue = [root], result = [];
while(queue.length){
let timeStack = [],queueLength = queue.length;
for(let i = 0;i<queueLength;i++){
const timeVariable = queue.shift();
timeStack.push(timeVariable.value)
timeVariable.left && queue.push(timeVariable.left)
timeVariable.right && queue.push(timeVariable.right)
}
result.push(timeStack)
}
return result
}
测试
const testData = {
value: 8,
left: {
value: 2,
left: { value: 1, left: null, right: null },
right: { value: 5, left: null, right: null }
},
right: {
value: 12,
left: { value: 10, left: null,
right: { value: 11, left: null, right:null }
},
right: { value: 16, left: null, right: null }
}
}
console.log(levelOrdder(testData),'levelOrdder')
//[ [ 8 ], [ 2, 12 ], [ 1, 5, 10, 16 ], [ 11 ] ] levelOrdder
二叉树源码实现
基类
树的标准存储元素有:自身值、左节点和右节点;默认左右节点值为null
let Node = function(value){
this.value = value;
this.left = null;
this.right = null;
}
插入元素 .insert
let root = null;
let insertNode = function(node,newNode){
if(newNode.value > node.value){
if(node.right == null){
node.right = newNode
} else {
insertNode(node.right,newNode)
}
} else if(newNode.value < node.value){
if(node.left == null){
node.left = newNode
} else {
insertNode(node.left,newNode)
}
}
}
this.insert = function(value) {
let newNode = new Node(value);
if(root == null){
root = newNode;
} else {
insertNode(root,newNode)
}
}
遍历 .traverse
let traverse = function(node,cb){
if(node == null) return;
// cb(node.value) 8 2 3 9
traverse(node.left,cb)
// cb(node.value) 2 3 8 9
traverse(node.right,cb)
cb(node,value) //3 2 9 8
}
this.traverse = function(cb){
traverse(root,cb)
}
最值 .min .max
let min = function(node){
if(node == null) return null
while(node && node.left) node = node.left;
return node.value
}
let max = function(node){
if(node == null) return null
while(node && node.right) node = node.right;
return node.value
}
this.min = function(){
return min(root)
}
this.max = function(){
return max(root)
}
移除节点 .remove
let findMinNode = function(node){
if(node == null) return null;
while(node && node.left){
node = node.left
}
return node
}
let removeNode = function(node,value){
if(node == null) return null;
if(value > node.value){
//向右遍历查找
node.right = removeNode(node.right,value)
return node
} else if(value < node.value){
//向左遍历查找
node.left = removeNode(node.left,value);
return node
} else {
// value == node.value
if(node.left == null && node.right == null){
//叶子节点
node = null;
return node
}
if(node.left == null && node.right){
//只有右节点
return node.right
}
if(node.left && node.right == null){
return node.left
}
//左右节点都在 - 查找右节点的最小节点
let RMinNode = findMinNode(node.right);
node.value = RMinNode.value
return node
}
}
this.remove = function(value){
root = removeNode(root,value)
}
获取根节点值 .getNode
this.getNode = function(){
return root
}
获取节点信息
let searchNode = function (node, value) {
if (node === null) false;
// console.log(node, "node=========");
if (node.value > value) {
// 目标值较小,向左急速查找
return searchNode(node.left, value);
} else if (node.value < value) {
// 目标值较大 继续向右查找
return searchNode(node.right, value);
} else {
// 遍历值和目标值相同 找到
return node;
}
};
this.searchNode = function (node, value) {
return searchNode(node, value);
};
完整代码
let tree = function() {
let Node = function(value){
this.value = value;
this.left = null;
this.right = null;
}
let root = null;
let insertNode = function(node,newNode){
if(newNode.value > node.value){
if(node.right == null){
node.right = newNode
} else {
insertNode(node.right,newNode)
}
} else if(newNode.value < node.value){
if(node.left == null){
node.left = newNode
} else {
insertNode(node.left,newNode)
}
}
}
this.insert = function(value) {
let newNode = new Node(value);
if(root == null){
root = newNode;
} else {
insertNode(root,newNode)
}
}
let traverse = function(node,cb){
if(node == null) return;
// cb(node.value) 8 2 3 9
traverse(node.left,cb)
// cb(node.value) 2 3 8 9
traverse(node.right,cb)
cb(node,value) //3 2 9 8
}
this.traverse = function(cb){
traverse(root,cb)
}
let min = function(node){
if(node == null) return null
while(node && node.left) node = node.left;
return node.value
}
let max = function(node){
if(node == null) return null
while(node && node.right) node = node.right;
return node.value
}
this.min = function(){
return min(root)
}
this.max = function(){
return max(root)
}
this.getNode = function(){
return root
}
let findMinNode = function(node){
if(node == null) return null;
while(node && node.left){
node = node.left
}
return node
}
let removeNode = function(node,value){
if(node == null) return null;
if(value > node.value){
//向右遍历查找
node.right = removeNode(node.right,value)
return node
} else if(value < node.value){
//向左遍历查找
node.left = removeNode(node.left,value);
return node
} else {
// value == node.value
if(node.left == null && node.right == null){
//叶子节点
node = null;
return node
}
if(node.left == null && node.right){
//只有右节点
return node.right
}
if(node.left && node.right == null){
return node.left
}
//左右节点都在 - 查找右节点的最小节点
let RMinNode = findMinNode(node.right);
node.value = RMinNode.value
return node
}
}
this.remove = function(value){
root = removeNode(root,value)
}
let searchNode = function (node, value) {
if (node === null) false;
// console.log(node, "node=========");
if (node.value > value) {
// 目标值较小,向左急速查找
return searchNode(node.left, value);
} else if (node.value < value) {
// 目标值较大 继续向右查找
return searchNode(node.right, value);
} else {
// 遍历值和目标值相同 找到
return node;
}
};
this.searchNode = function (node, value) {
return searchNode(node, value);
};
}
测试代码
let as = new tree()
as.insert(8)
as.insert(2)
as.insert(3)
as.insert(12)
console.log(as.getNode(),'=========',as.max(),as.min())
as.remove(3)
as.insert(5)
as.insert(1)
as.insert(16)
as.insert(10)
console.log(as.getNode(),'=========',as.max(),as.min())
// 单独
console.log(as.searchNode(target, 8), "=========", as.max(), as.min());
// Node {
// value: 8,
// left: Node {
// value: 2,
// left: Node { value: 1, left: null, right: null },
// right: Node { value: 3, left: null, right: [Node] }
// },
// right: Node {
// value: 12,
// left: Node { value: 10, left: null, right: null },
// right: Node { value: 16, left: null, right: null }
// }
// } ========= 16 1
树的转化
二叉树转换为数组
数组转化为二叉树
通过传入的数组得到中间值,然后以中间值为树根进行递归遍历赋值当前树根对的左右节点,递归的过程中,会不断的产生临时根节点,只在首次取中间值时产生的值为树根,其他的都为树根的子节点;
function TreeNode(val) {
this.value = val;
}
var sortedArrayToBST = function (nums) {
if (nums.length === 0) {
return null;
}
if (nums.length === 1) {
return new TreeNode(nums[0]);
}
var mid = parseInt(nums.length / 2); // 计算中间位置,数组下标从0开始,所以parseInt取整
var root = new TreeNode(nums[mid]); // 中间位置的元素作为树根
root.left = sortedArrayToBST(nums.slice(0, mid)); // 递归生成树的左子树
root.right = sortedArrayToBST(nums.slice(mid + 1)); // 递归生成树的右子树
return root; // 递归结束后返回树
};
树转化为二叉树
- 步骤
- 连接所有兄弟节点;
- 对数中的每一个节点,值保留与第一个节点的连线,其他的删除 -- 保留最长子节点连线;
- 整棵树顺时针旋转90度
二叉树转化为树;
- 步骤
- 将左孩子的右孩子、右孩子的右孩子……全部连接起来;
- 所有双亲节点删除与右孩子的连线;
- 调整一定角度
森林转化为二叉树
- 步骤
- 先将森林中每棵树转化为二叉树;
- 将二叉树根节点视为兄弟节点连接起来;
- 调整一定角度