您的位置:首页 > 理论基础 > 数据结构算法

数据结构学习笔记:二叉树

2017-09-23 12:27 507 查看

二叉树

二叉树:每个节点最多有两个子树的树结构。

二叉树的性质:

二叉树的第i层最多有2^(i-1)个子节点

深度为k的二叉树最多共有 2^k-1个节点

对于任意一颗二叉树,如果其叶子节点数为n0,度为2的节点数为n2,那么n0 = n2+1

满二叉树:深度为k且有2^k-1个节点的二叉树为满二叉树。

完全二叉树:深度为k,有n个节点的二叉树,当且仅当其每个节点都与深度为k的满二叉树中编号从1到n的节点一一对应,称之为完全二叉树。(除最后一层外,每一层上的节点均达到最大值,最后一层只缺少右边的若干节点)。

二叉树的实现(Java):

public TreeNode{
public TreeNode left;
public TreeNode right;
public int val;
public TreeNode(val){
this.val = val;
this.left = null;
this.right = null;
}
}


遍历

深度优先:

前序:根左右

中序:左根右

后序:左右根

广度优先

前/中/后序遍历使用递归,也就是的思想对二叉树进行遍历,广度优先一般使用队列的思想对二叉树进行遍历。

如果已知中序遍历和前序遍历或者中序遍历和后序遍历,那么就可以完全恢复出原二叉树结构。

其中最为关键的是前序遍历中第一个一定是根,而后序遍历最后一个一定是根,中序遍历在得知根节点后又可进一步递归得知左右子树的根节点。

但是这种方法也是有适用范围的:元素不能重复!否则无法完成定位。

对于树的三种深度优先遍历的实现方法均有递归和迭代两种实现,递归比较简单,而迭代需要用到栈。

前序preorder

//递归版
public List<Integer> preorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
helper(res,root);
return res;
}
private void helper(List<Integer> res, TreeNode root){
if(root == null) return;
res.add(root.val);
helper(res,root.left);
helper(res,root.right);
}


//迭代版
public List<Integer> preorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
if(root == null)return res;
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
res.add(node.val);
if(node.right!=null)stack.push(node.right);
if(node.left!=null) stack.push(node.left);
}
return res;
}


中序inorder

//递归版
public List<Integer> inorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
helper(res,root);
return res;
}
private void helper(List<Integer> res, TreeNode root){
if(root == null) return;
helper(res,root.left);
res.add(root.val);
helper(res,root.right);
}


//迭代版
public List<Integer> inorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
if(root == null)return res;
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode node = root;
while(node!=null || !stack.isEmpty()){
if(node! = null){
stack.push(node);
node = node.left;
}else{
node = stack.pop();
res.add(node.val);
node = node.right;
}
}
return res;
}


后序postorder

//递归版
public List<Integer> postorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
helper(res,root);
return res;
}
private void helper(List<Integer> res, TreeNode root){
if(root == null) return;
helper(res,root.left);
helper(res,root.right);
res.add(root.val);
}


//迭代版
public List<Integer> postorder(TreeNode root){
List<Integer> res = new ArrayList<Integer>();
if(root == null)return res;
stack.push(stack);
TreeNode prev = null;
while(!stack.isEmpty()){
TreeNode curr = stack.peek();
if(prev == null || prev.left == curr || prev.right == curr){
if(curr.left!=null){
stack.push(curr.left);
}else if(curr.right!=null){
stack.push(curr.right);
}
}else if(curr.left == prev){
if(curr.right!=null){
stack.push(curr.right);
}
}else{
stack.pop();
res.add(curr.val);
}
prev = curr;
}
return res;
}


层次遍历level order

public List<ArrayList<Integer>> levelOrder(TreeNode root) {
List<ArrayList<Integer>> res = new List<ArrayList<Integer>>();
if(root == null) return res;
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while(!queue.isEmpty()){
int size = queue.size();
ArrayList<Integer> list = new ArrayList<Integer>();
while(size>0){
TreeNode node = queue.poll();
list.add(node.val);
if(node.left!=null) queue.offer(node.left);
if(node.right!=null) queue.offer(node.right);
size--;
}
res.add(list);
}
return res;
}


最大堆:如果一棵完全二叉树的任意一个非终端节点的元素都不小于其左儿子节点和右儿子节点,则此完全二叉树为最大堆最大堆的根节点是整个堆中最大的。

最小堆:如果一棵完全二叉树的任意一个非终端节点的元素都不大于其左儿子节点和右儿子节点,则此完全二叉树为最小堆最小堆的根节点是整个堆中最小的。

堆的性质:

n个节点的堆的高度h为[logn](向下取整)。

对于堆中的任意节点v,其下标为i

若它有左孩子,则左孩子的下标为
2*i+1


若它有右孩子,则右孩子的下标为
2*i+2


若它有父节点,则父节点的下标为
floor((i-1)/2)


堆的几个基本操作:

元素插入

插入算法分为两个步骤:

将元素加入到末尾

将元素上浮

上浮操作主要是将元素插入到正确的位置,假设要插入的元素为e:

如果堆非空,元素e一定有父节点p,如果
e<p
,则不用做任何调整;但若
e>p
,则将e与p的位置交换

此后重复上面的步骤,即比较e与其父节点,若
e>p
则交换二者,直至
e<p
.

时间复杂度:O(logn),n为堆中节点总数量。由于上浮过程最多交换的次数不超过堆的高度logn,因此时间复杂度为O(logn)。

//插入元素
public void add(int item){
heap.add(item);
siftUp(heap.size()-1);
}


//对第i个元素进行上浮
public void siftUp(int index){
int e = heap.get(index);
while(index>0){
int pindex = (index-1)/2;
int parent = heap.get(pindex);
if(e>parent){
//交换二者
heap.set(index, parent);
index = pindex;
}else break;
}
heap.set(index,e);
}


元素删除

一般堆中的删除指的是删除堆顶元素。

分两个步骤:

将堆顶元素取出,并将堆的最后一个元素放到堆顶。

对新堆顶进行下沉调整

下沉操作与上浮相反,是要将当前位置的元素下沉至正确的位置。

若新堆顶不满足堆序,则将其与两个孩子中的较大者交换位置

重复以上步骤

时间复杂度:O(logn)。n为堆中节点总数量。由于下沉过程最多交换的次数不超过堆的高度logn,因此时间复杂度为O(logn)。

//删除元素
public int deleteTop(){
if(heap.size() == 0) return -1;

int max = heap.get(0);
int last  = heap.remove(heap.size()-1);
if(heap.size() == 0) return last;

heap.set(0,last);
siftDown(0);
return max;
}


//下沉
public void siftDown(index){
int e = heap.get(index);
int l_index = index*2+1;

while(l_index <heap.size()){
//记录最大的孩子与最大孩子的下标
int maxChild = heap.get(l_index );
int maxIndex = l_index;

int r_index = l_index+1;
if(r_index <heap.size()){
int right = heap.get(r_index);
if(right>maxChild ){
maxChild = right;
maxIndex = r_index;
}
}
if(maxChild>e){
heap.set(index,maxChild);
index = maxIndex;
l_index = index*2+1;
}else break;
}

heap.set(index,e);
}


建堆

可以从空堆开始反复调用add接口来将所有元素插入,这种方法的时间复杂度为O(nlogn)

O(log1)+O(log2)+O(log3)+O(log4)+……+O(logn) = O(logn!) = O(nlogn)


堆排序

建堆

其实是上面的deleteTop操作,将堆顶元素与堆尾元素交换,而后重新建堆,重复此过程。

//建堆
public void heapSort(int[] arr){
//从第一个非叶子结点开始建堆
int last = arr.length-1;
int index = (last-1)/2;
for(int i=index;i>=0;i--){
maxifyHeap(arr,i,arr.length-1);
}

//每次将堆顶元素与堆尾元素交换,并重新建堆
for(int i=arr.length-1;i>0;i--){
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
maxifyHeap(arr,0,i-1);
}
}

//调整堆,下沉过程
public void maxifyHeap(int[] arr,int i, int size){
int l_index = i*2+1;
while(l_index<size){
int maxChild = arr[l_index];
int max_index = l_index;
int r_index = l_index+1;
if(r_index<size){
int right = arr[r_index];
if(right>maxChild){
maxChild = right;
max_index = r_index;
}
}
if(maxChild>arr[i]){
arr[max_index] = arr[i];
arr[i] = maxChild;
i = max_index;
l_index = i*2+1;
}else break;
}
}


时间复杂度:O(nlogn)

建堆的时间为O(nlogn)。

每一次对栈顶元素的下沉调整为O(logn),共调整n次,因此时间为O(nlogn)。

哈夫曼树

给定n个权值做为n的叶子节点,构造一个二叉树,使得带权路径长度最小。这样的二叉树为最优二叉树,也为哈夫曼树

假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:

将w1、w2、…,wn看成是有 n 棵树的森林(每棵树仅有一个结点);

在森林中选中两个权值最小的根节点合并,新树的根节点的权值为其左右节点权值之和。

从森林中删除所选的两个树,并将新树加入森林;

重复2,3两步,直到森林中只剩一棵树。



二叉查找树BST

二叉查找树(Binary Search Tree),亦称二叉搜索树,又称二叉排序树。

BST性质:

若左子树不空,则左子树上所有节点的值均小于其根节点的值

若右子树不空,则右子树上所有节点的值均大于其根节点的值

左、右子树也为BST

没有键值相等的节点

二分查找的时间复杂度是O(log(n)),最坏情况下的时间复杂度是O(n)(相当于顺序查找)。

平衡二叉树

又称AVL树,有如下性质:

它的左子树和右子树都是AVL

左子树和右子树高度之差的绝对值不超过1

平衡二叉树是对二叉搜索树的改进,二叉搜索树的缺点就是树的结构无法预料,最坏情况下可能是一个单支二叉树,其高度和节点数相同,就相当于一个单链表,此时查找的时间复杂度最差,为O(n)。

Trie树

Trie树,即字典树

它有3个基本性质:

根节点不包含字符,除根节点外每一个节点都只包含一个字符。

从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。

每个节点的所有子节点包含的字符都不相同。



典型应用是用于统计和排序大量的字符串

词频统计:在内存有限的情况下,可能不好使用hash来统计词频,因此可以用trie树来减少空间的使用,因为公共前缀都是用一个节点保存的。

前缀匹配:就拿上面的图来说吧,如果我想获取所有以”a”开头的字符串,从图中可以很明显的看到是:and,as,at,如果不用trie树,你该怎么做呢?很显然朴素的做法时间复杂度为O(N2) ,那么用Trie树就不一样了,它可以做到h,h为你检索单词的长度,
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  数据结构 二叉树