前面我们讲的都是线性表结构,栈、队列等等。今天我们讲一种非线性表结构,树。树这种数据结构比线性表的数据结构要复杂得多
树(Tree)
A 节点就是 B 节点的父节点,B 节点是 A 节点的子节点。B、C、D 这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫做根节点,也就是图中的节点 E。
关于“树”,还有三个比较相似的概念:高度(Height)、深度(Depth)、层(Level)。
二叉树(Binary Tree)
二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。
叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫做满二叉树。
叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。
要理解完全二叉树定义的由来,我们需要先了解,如何表示(或者存储)一棵二叉树?
一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。
链式存储法
每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。
顺序存储法
如果节点 X 存储在数组中下标为 i 的位置,下标为 2 _ i 的位置存储的就是左子节点,下标为 2 _ i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。
如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这也是为什么完全二叉树会单独拎出来的原因,也是为什么完全二叉树要求最后一层的子节点都靠左的原因。
二叉树的遍历
经典的方法有三种,前序遍历、中序遍历和后序遍历。其中,前、中、后序,表示的是节点与它的左右子树节点遍历打印的先后顺序。
- 前序遍历是指,对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
- 中序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
- 后序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。
// 先序遍历
const preOrder = (root) => {
if (!root) {
return;
}
console.log(root.val);
preOrder(root.left);
preOrder(root.right);
};
// 中序遍历
const inOrder = (root) => {
if (!root) {
return;
}
inOrder(root.left);
console.log(root.val);
inOrder(root.right);
};
// 后序遍历
const postOrder = (root) => {
if (!root) {
return;
}
postOrder(root.left);
postOrder(root.right);
console.log(root.val);
};
你知道二叉树遍历的时间复杂度是多少吗?我们一起来看看。
从我前面画的前、中、后序遍历的顺序图,可以看出来,每个节点最多会被访问两次,所以遍历操作的时间复杂度,跟节点的个数 n 成正比,也就是说二叉树遍历的时间复杂度是 O(n)。
二叉查找树(Binary Search Tree)
二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。不过,它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。
二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
class Node {
constructor(data) {
this.data = data;
this.left = null;
this.right = null;
}
}
class BinarySearchTree {
constructor() {
// root of a binary search tree
this.root = null;
}
// helper method which creates a new node to
// be inserted and calls insertNode
insert(data) {
// Creating a node and initialising
// with data
var newNode = new Node(data);
// root is null then node will
// be added to the tree and made root.
if (this.root === null) this.root = newNode;
// find the correct position in the
// tree and add the node
else this.insertNode(this.root, newNode);
}
// Method to insert a node in a tree
// it moves over the tree to find the location
// to insert a node with a given data
insertNode(node, newNode) {
// if the data is less than the node
// data move left of the tree
if (newNode.data < node.data) {
// if left is null insert node here
if (node.left === null) node.left = newNode;
// if left is not null recur until
// null is found
else this.insertNode(node.left, newNode);
}
// if the data is more than the node
// data move right of the tree
else {
// if right is null insert node here
if (node.right === null) node.right = newNode;
// if right is not null recur until
// null is found
else this.insertNode(node.right, newNode);
}
}
// helper method that calls the
// removeNode with a given data
remove(data) {
// root is re-initialized with
// root of a modified tree.
this.root = this.removeNode(this.root, data);
}
// Method to remove node with a
// given data
// it recur over the tree to find the
// data and removes it
removeNode(node, key) {
// if the root is null then tree is
// empty
if (node === null) return null;
// if data to be delete is less than
// roots data then move to left subtree
else if (key < node.data) {
node.left = this.removeNode(node.left, key);
return node;
}
// if data to be delete is greater than
// roots data then move to right subtree
else if (key > node.data) {
node.right = this.removeNode(node.right, key);
return node;
}
// if data is similar to the root's data
// then delete this node
else {
// deleting node with no children
if (node.left === null && node.right === null) {
node = null;
return node;
}
// deleting node with one children
if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
// Deleting node with two children
// minimum node of the right subtree
// is stored in aux
var aux = this.findMinNode(node.right);
node.data = aux.data;
node.right = this.removeNode(node.right, aux.data);
return node;
}
}
// Performs inorder traversal of a tree
inorder(node) {
if (node !== null) {
this.inorder(node.left);
console.log(node.data);
this.inorder(node.right);
}
}
// Performs preorder traversal of a tree
preorder(node) {
if (node !== null) {
console.log(node.data);
this.preorder(node.left);
this.preorder(node.right);
}
}
// Performs postorder traversal of a tree
postorder(node) {
if (node !== null) {
this.postorder(node.left);
this.postorder(node.right);
console.log(node.data);
}
}
// finds the minimum node in tree
// searching starts from given node
findMinNode(node) {
// if left of a node is null
// then it must be minimum node
if (node.left === null) return node;
else return this.findMinNode(node.left);
}
// returns root of the tree
getRootNode() {
return this.root;
}
// search for a node with given data
search(node, data) {
// if trees is empty return null
if (node === null) return null;
// if data is less than node's data
// move left
else if (data < node.data) return this.search(node.left, data);
// if data is more than node's data
// move right
else if (data > node.data) return this.search(node.right, data);
// if data is equal to the node data
// return node
else return node;
}
}
二叉查找树的时间复杂度分析
平衡二叉查找树的高度接近 logn,所以插入、删除、查找操作的时间复杂度也比较稳定,是 O(logn)。
原文链接: https://jesse121.github.io/blog/articles/2021/03/15.html
版权声明: 转载请注明出处.