总结
从宏观上看,数据、数据元素和数据项实际上反映了数据组织的三个层次,数据可由若干个数据元素组成,而数据元素又可由若干个数据项组成。
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。它包括数据的逻辑结构、数据的存储结构和数据的基本运算。
数据的逻辑结构
树是一种非线性结构,树与线性表的逻辑关系不同。
树(Tree)是一类重要的数据结构,其定义如下:
树:是n(n≥0)个结点(元素)构成的有限集合。当n=0时,称这棵树为空树。在一棵非空树T中:
结点:是数据元素的别名。
集合:线性表对应的是序列,树对应的是集合。
结点的度:树中结点所拥有的子树的个数。
树的度:树中各结点度的最大值。
二叉树的度为2。
叶子结点(终端结点):度为0的结点。
分枝结点(非终端结点):度不为0的结点。
孩子:树中一个结点的子树的根结点。
双亲:树中一个孩子结点的上层结点(唯一)。
兄弟:具有同一个双亲的孩子结点互成为兄弟。
子孙:一个结点的所有子树中的结点称之为该结点的子孙节点。
祖先:从根结点到该结点所经分支上的所有结点。
层数:根结点的层数规定为1,其余结点的层数等于它的双亲结点的层数+1。
深度:所有结点的最大层次。
堂兄弟:双亲在同一层,且双亲不同的结点互为堂兄弟。
有序树:如果一棵树中结点的各子树从左到右是有次序的,即若交换了某结点各子树的相对位置则构成不同的树,称这棵树为有序树;反之,则称为无序树。
无序树:若树中各结点的子树是无次序的,可以互换,则称为无序树。
森林:m(m≥0)棵不相交的树的集合称为森林。自然界中树和森林是不同的概念,但在数据结构中,树和森林只有很小的差别,任何一棵树,删去根结点就变成了森林。
注意事项:
递归方法是树结构算法的基本特点
树的根节点唯一
n>0:子树的个数从定义上没有限制,可以很大。
子集互不相交:
树的逻辑表示**(了解)**
直观表示法
形式化表示法
广义表表示法 - (A(B(E(J),F),C(G(K,L)),D(H,I)))
森林
m棵互不相交的树的集合**(m=>0)**
可见,蓝色、绿色、黄色各为一棵树,它们不互相交,共同构成了一个森林,但这个森林本身也是一棵树。
树的存储结构
注意:二叉树是有序树,分左右!!
五种基本状态
几个特殊的二叉树
真二叉树
斜树
满二叉树和完全二叉树
按层序编号,则可以通过一个结点知道其双亲结点和孩子结点的编号。
满二叉树
对二叉树来说,第i层的节点个数最多为2^(i-1)个,如果二叉树的每一层的节点个数都达到最大值,即叶子节点全部在最后一层,非叶子节点一定有左右孩子,这种二叉树称为满二叉树。
完全二叉树
完全二叉树就和名字有点不太一样了,除最后一层节点外,其他层节点都必须要有两个子节点,并且最后一层节点都要左排列。
完全二叉树的判断条件没有满二叉树那么苛刻,完全二叉树要求最下面两层的节点,可以没有孩子节点,也可以仅有一个孩子,但最下层的叶子节点必须都在左侧。
由于顺序存储二叉树的空间利用率较低,因此二叉树一般都采用链式存储结构,用链表结点来存储二叉树中的每个结点。在二叉树中,结点结构通过包括若干数据域和若干指针域,二叉链表至少包含3个域:数据域 data、左指针域 lchild和右指针域 rchild,如下图所示:
因此,上图对应的链式存储结构如下图所示:
用顺序存储方式数组下标最好从1开始,这样才能利用完全二叉树的性质。
二叉树的顺序存储
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映结点之间的逻辑关系这样既能最大可能地节省存储空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
private List<Integer> res = new ArrayList<>();
先序遍历
递归
private void dfs(TreeNode node) {
if (node == null) {
return;
}
res.add(node.val);
dfs(node.left);
dfs(node.right);
}
非递归
public List<Integer> preorderTraversal_Iteration(TreeNode root) {
if (root == null) {
return res;
}
Deque<TreeNode> stack = new LinkedList<>();
stack.addFirst(root);
while (!stack.isEmpty()) {
TreeNode node = stack.removeFirst();
res.add(node.val);
//注意这里要先加右子节点,再加左子节点,这样出栈的时候才是先左后右
if (node.right != null) {
stack.addFirst(node.right);
}
if (node.left != null) {
stack.addFirst(node.left);
}
}
return res;
}
中序遍历
递归
private void dfs(TreeNode node) {
if (node == null) {
return;
}
dfs(node.left);
res.add(node.val);
dfs(node.right);
}
非递归
public List<Integer> inorderTraversal_Iteration(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<>();
while (root != null || !stack.isEmpty()) {
//先一股脑找到该节点的最左子节点
while (root != null) {
stack.addFirst(root);//期间将路径中的节点压入栈中
root = root.left;
}
root = stack.removeFirst();
res.add(root.val);
root = root.right;//转到右子节点
}
return res;
}
后序遍历
递归
private void dfs(TreeNode node) {
if (node == null) {
return;
}
dfs(node.left);
dfs(node.right);
res.add(node.val);
}
非递归
public List<Integer> postorderTraversal_Iteration(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<>();
TreeNode prev = null;
while (root != null || !stack.isEmpty()) {
//root==null时不进行while循环:来自下面的if,上一轮右子树为空或是右子树已遍历
while (root != null) {//一直遍历到该节点的最左子节点的左子节点(null)
stack.addFirst(root);
root = root.left;
}
/*
* 这时出栈的元素有两种情况:
* 1.一直遍历到的最左子节点;
* 2.新弹出一个已存储的节点;
*/
root = stack.removeFirst();
if (root.right == null || root.right == prev) {//右子树为空或是右子树已遍历
res.add(root.val);//加入该节点
prev = root;//标记该节点已遍历
root = null;//root置为null,下一轮可以从栈中弹出新节点
} else {
stack.addFirst(root);//因为右子树存在,把弹出的节点再放回去
root = root.right;//转到右子节点
}
}
return res;
}
层次遍历
广度优先是一种横向的获取方式,先从树的较浅层开始获取节点,直接获取完同层次的所有页面后才进入下一层。层次遍历是广度优先遍历,层次遍历是二叉树按照从根节点到叶子节点的层次关系,一层一层横向遍历各个节点。
二叉树的层次遍历,是指从二叉树的根结点的这一层开始,逐层向下遍历,在每一层上按从左到右的顺序对结点逐个访问。层次遍历可以用一个队列来实现。
层次遍历的非递归借助了队列实现,队列的特点是先进先出。首先访问根节点并进队,队列不为空时出队并输出节点的值,如果当前出队节点有左孩子则左孩子先进队,如果有右孩子则右孩子后进队。
递归
private static class Node {
int val;
Node left;
Node right;
public Node(int data) {
this.val = val;
}
}
private void levelRecursion(Node node,
LinkedList<List<Integer>> result, int level) {
if (node == null) {
return;
}
if (result.size() < level + 1) {// 说明还需要添加一行
result.addFirst(new ArrayList<Integer>());
}
result.get(result.size() - 1 - level).add(node.val);
levelRecursion(node.left, result, level + 1);
levelRecursion(node.right, result, level + 1);
}
public List<List<Integer>> levelOrderBottom2(Node root) {
LinkedList<List<Integer>> result = new LinkedList<List<Integer>>();
levelRecursion(root, result, 0);
return result;
}
非递归
private static class TreeNode {
int data;
TreeNode leftChild;
TreeNode rightChild;
public TreeNode(int data) {
this.data = data;
}
}
public static void levelOrderTraversal (TreeNode node) {
Queue<TreeNode> queue = new LinkedList<TreeNode>();
//将指定的元素插入到这个队列中
queue.offer(node);
while (!queue.isEmpty()) {
//获取并移除该队列的头
node = queue.poll();
System.out.print(node.data + " ");
if (node.leftChild != null) {
queue.offer(node.leftChild);
}
if (node.rightChild != null) {
queue.offer(node.rightChild);
}
}
}
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
四种遍历方式中,无论是递归还是非递归,二叉树的每个结点都只被访问一次,所以对于 n个结点的二叉树,时间复杂度均为 O ( n )。
除层序遍历外的其它三种遍历方式,所需辅助空间为遍历过程中栈的最大容量,也就是二叉树的深度,所以空间复杂度与二叉树的形状有关。对于 n 个结点的二叉树,最好情况下是完全二叉树,空间复杂度 O ( l o g 2 n ) ;最坏情况下对应斜二叉树,空间复杂度 O ( n ) 。层序遍历所需辅助空间为遍历过程中队列的最大长度,也就是二叉树的最大宽度 k,所以空间复杂度 O(k)。
从上面二叉树的遍历可以看出:如果二叉树中各结点值不同,那么先序、中序、以及后序遍历所得到的序列也是唯一。反过来,如果知道任意两种遍历序列,能否唯一确定这棵二叉树?
首先明确:只有先序中序,后序中序这两种组合可以唯一确定,先序后序不能。
如果知道的是先序中序,那么根据先序遍历先访问根结点的特点在中序序列中找到这个结点,该结点将中序序列分成两部分,左边是这个根结点的左子树中序序列,右边是这个根结点的右子树中序序列。根据这个序列,在先序序列中找到对应的左子序列和右子序列,根据先序遍历先访问根的特点又可以确定两个子序列的根结点,并根据这两个根结点可以将上次划分的两个中序子序列继续划分,如此下去,直到取尽先序序列中的结点时,便可得到对应的二叉树。
后序中序同理,区别是先序序列中第一个结点是根结点,而后序序列中最后一个结点是根结点。
先序和后序不能确定的原因在于不能确定根结点的左右子树。 比如先序序列AB,后序序列BA,根结点是A可以确定,但B是A的左子结点还是右子结点就确定不了了。